From c2f08ff199d6a04908158a66803a6854b8de224d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Aug 2026 09:25:10 +0000 Subject: [PATCH 01/60] docs: update version references to 1.0.13-preview.2 --- java/README.md | 8 ++++---- java/sdk/jbang-example.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java/README.md b/java/README.md index 10eb72ce50..2e1ba51812 100644 --- a/java/README.md +++ b/java/README.md @@ -32,14 +32,14 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. com.github copilot-sdk-java - 1.0.13-preview.1 + 1.0.13-preview.2 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.13-preview.1' +implementation 'com.github:copilot-sdk-java:1.0.13-preview.2' ``` #### Snapshot Builds @@ -58,7 +58,7 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.14-preview.1-SNAPSHOT + 1.0.14-preview.2-SNAPSHOT ``` @@ -67,7 +67,7 @@ Snapshot builds of the next development version are published to Maven Central S Replace `${copilot.sdk.version}` with the latest release from Maven Central. ```groovy -implementation 'com.github:copilot-sdk-java:1.0.14-preview.1-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.14-preview.2-SNAPSHOT' ``` ## In-process mode (experimental) diff --git a/java/sdk/jbang-example.java b/java/sdk/jbang-example.java index cf091d3c12..6cb04215ac 100644 --- a/java/sdk/jbang-example.java +++ b/java/sdk/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.13-preview.1 +//DEPS com.github:copilot-sdk-java:1.0.13-preview.2 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; From a94a1f2ddd7e015b3d3f7bbc94c7c2b1a28219e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Aug 2026 09:25:41 +0000 Subject: [PATCH 02/60] [maven-release-plugin] prepare release java/v1.0.13-preview.2 --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index f3694457d8..e0db057863 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.1-SNAPSHOT + 1.0.13-preview.2 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.2 diff --git a/java/pom.xml b/java/pom.xml index c5a17d66d8..dfa6bb7484 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.1-SNAPSHOT + 1.0.13-preview.2 pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.2 diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index 4f9ece6354..e0265813c5 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.1-SNAPSHOT + 1.0.13-preview.2 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.2 From 50ce37e19258524c6de82651652971e96d7ae5f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Aug 2026 09:25:45 +0000 Subject: [PATCH 03/60] [maven-release-plugin] prepare for next development iteration --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index e0db057863..d138a0e56b 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.2 + 1.0.14-preview.2-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.2 + HEAD diff --git a/java/pom.xml b/java/pom.xml index dfa6bb7484..3e5e1645a5 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.2 + 1.0.14-preview.2-SNAPSHOT pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.2 + HEAD diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index e0265813c5..811a05db67 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.2 + 1.0.14-preview.2-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.2 + HEAD From 53efb3593e65e5f04474099ffd3e00efa7777482 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 04:06:49 +0000 Subject: [PATCH 04/60] Update @github/copilot to 1.0.82 (#2442) * Update @github/copilot to 1.0.82 - Updated nodejs and test harness dependencies - Re-ran code generators - Formatted generated code * Fix generated schema compatibility regressions Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Stephen Toub Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 73 ++++++++- dotnet/src/Generated/SessionEvents.cs | 151 ++++++++++++++++++ go/rpc/zrpc.go | 58 +++++++ go/rpc/zsession_events.go | 21 +++ go/zsession_events.go | 7 + java/pom.xml | 2 +- java/scripts/codegen/package-lock.json | 72 ++++----- java/scripts/codegen/package.json | 2 +- .../AssistantMessageToolRequest.java | 4 +- .../AssistantMessageToolRequestCaller.java | 29 ++++ ...AssistantMessageToolRequestCallerType.java | 33 ++++ .../github/copilot/generated/AutoTier.java | 37 +++++ .../copilot/generated/SessionResumeEvent.java | 2 + .../copilot/generated/SessionStartEvent.java | 2 + .../copilot/generated/rpc/AutoTier.java | 37 +++++ .../generated/rpc/CapiSessionOptions.java | 2 + .../rpc/SessionCommandsEnqueueParams.java | 4 +- .../copilot/generated/rpc/SessionRpc.java | 3 + .../generated/rpc/SessionSandboxApi.java | 42 +++++ ...sionSandboxGetEnforcementStatusParams.java | 30 ++++ ...sionSandboxGetEnforcementStatusResult.java | 34 ++++ .../copilot/SessionEventHandlingTest.java | 4 +- nodejs/package-lock.json | 54 +++---- nodejs/package.json | 2 +- nodejs/samples/package-lock.json | 2 +- nodejs/src/generated/rpc.ts | 38 ++++- nodejs/src/generated/session-events.ts | 27 ++++ nodejs/test/session-event-codegen.test.ts | 29 +++- python/copilot/generated/rpc.py | 97 +++++++---- python/copilot/generated/session_events.py | 56 +++++++ rust/src/generated/api_types.rs | 72 ++++++++- rust/src/generated/rpc.rs | 43 +++++ rust/src/generated/session_events.rs | 48 ++++++ rust/tests/e2e/commands.rs | 1 + rust/tests/e2e/rpc_queue.rs | 3 + scripts/codegen/python.ts | 12 +- test/harness/package-lock.json | 54 +++---- test/harness/package.json | 2 +- 38 files changed, 1051 insertions(+), 138 deletions(-) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCaller.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCallerType.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/AutoTier.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTier.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusResult.java diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index f3c1a57e6e..c97660f369 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -5393,6 +5393,32 @@ internal sealed class LogRequest public string? Url { get; set; } } +/// Managed sandbox enforcement state for a session. +[Experimental(Diagnostics.Experimental)] +public sealed class SandboxEnforcementStatus +{ + /// Whether an enforcement failure has permanently blocked the session. + [JsonPropertyName("blocked")] + public bool Blocked { get; set; } + + /// The first sandbox enforcement failure that blocked the session. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Whether the effective managed policy requires an available sandbox backend. + [JsonPropertyName("required")] + public bool Required { get; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionSandboxGetEnforcementStatusRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Authentication status and account metadata for the session. [Experimental(Diagnostics.Experimental)] public sealed class SessionAuthStatus @@ -10933,6 +10959,10 @@ public sealed class OptionsUpdateAdditionalContentExclusionPolicy [Experimental(Diagnostics.Experimental)] public sealed class CapiSessionOptions { + /// Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. [JsonPropertyName("enableWebSocketResponses")] public bool? EnableWebSocketResponses { get; set; } @@ -13395,6 +13425,10 @@ internal sealed class EnqueueCommandParams [JsonPropertyName("command")] public string Command { get; set; } = string.Empty; + /// Optional user-facing text for the queue row. The command string is shown when omitted. + [JsonPropertyName("displayText")] + public string? DisplayText { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -30941,6 +30975,12 @@ internal SessionRpc(CopilotSession session) internal CopilotSession Session => _session; + /// Sandbox APIs. + public SandboxApi Sandbox => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// GitHubAuth APIs. public GitHubAuthApi GitHubAuth => field ?? @@ -31313,6 +31353,29 @@ public async Task LogAsync(string message, SessionLogLevel? level = n } } +/// Provides session-scoped Sandbox APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class SandboxApi +{ + private readonly CopilotSession _session; + + internal SandboxApi(CopilotSession session) + { + _session = session; + } + + /// Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. + /// The to monitor for cancellation requests. The default is . + /// Managed sandbox enforcement state for a session. + public async Task GetEnforcementStatusAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionSandboxGetEnforcementStatusRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.sandbox.getEnforcementStatus", [request], cancellationToken); + } +} + /// Provides session-scoped GitHubAuth APIs. [Experimental(Diagnostics.Experimental)] public sealed class GitHubAuthApi @@ -33687,14 +33750,15 @@ public async Task ExecuteAsync(string commandName, string /// Enqueues a slash command for FIFO processing on the local session. /// Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. + /// Optional user-facing text for the queue row. The command string is shown when omitted. /// The to monitor for cancellation requests. The default is . /// Indicates whether the command was accepted into the local execution queue. - public async Task EnqueueAsync(string command, CancellationToken cancellationToken = default) + public async Task EnqueueAsync(string command, string? displayText = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); _session.ThrowIfDisposed(); - var request = new EnqueueCommandParams { SessionId = _session.SessionId, Command = command }; + var request = new EnqueueCommandParams { SessionId = _session.SessionId, Command = command, DisplayText = displayText }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.commands.enqueue", [request], cancellationToken); } @@ -35592,6 +35656,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageStartData), TypeInfoPropertyName = "SessionEventsAssistantMessageStartData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageStartEvent), TypeInfoPropertyName = "SessionEventsAssistantMessageStartEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequest), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequest")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequestCaller), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequestCaller")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequestCallerType), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequestCallerType")] [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequestType), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequestType")] [JsonSerializable(typeof(GitHub.Copilot.AssistantReasoningData), TypeInfoPropertyName = "SessionEventsAssistantReasoningData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantReasoningDeltaData), TypeInfoPropertyName = "SessionEventsAssistantReasoningDeltaData")] @@ -35646,6 +35712,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchRequestedData), TypeInfoPropertyName = "SessionEventsAutoModeSwitchRequestedData")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchRequestedEvent), TypeInfoPropertyName = "SessionEventsAutoModeSwitchRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchResponse), TypeInfoPropertyName = "SessionEventsAutoModeSwitchResponse")] +[JsonSerializable(typeof(GitHub.Copilot.AutoTier), TypeInfoPropertyName = "SessionEventsAutoTier")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedOperation), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedOperation")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedStatus), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedStatus")] [JsonSerializable(typeof(GitHub.Copilot.BinaryAssetReference), TypeInfoPropertyName = "SessionEventsBinaryAssetReference")] @@ -36484,6 +36551,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SandboxConfigUserPolicyNetwork))] [JsonSerializable(typeof(SandboxConfigUserPolicyNetworkProxy))] [JsonSerializable(typeof(SandboxConfigUserPolicySeatbelt))] +[JsonSerializable(typeof(SandboxEnforcementStatus))] [JsonSerializable(typeof(ScheduleAddAtRequest))] [JsonSerializable(typeof(ScheduleAddCronRequest))] [JsonSerializable(typeof(ScheduleAddRequest))] @@ -36620,6 +36688,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionQueueRemoveMostRecentRequest))] [JsonSerializable(typeof(SessionQueueSnapshotRequest))] [JsonSerializable(typeof(SessionRemoteDisableRequest))] +[JsonSerializable(typeof(SessionSandboxGetEnforcementStatusRequest))] [JsonSerializable(typeof(SessionScheduleHasSelfPacedRequest))] [JsonSerializable(typeof(SessionScheduleHydrateRequest))] [JsonSerializable(typeof(SessionScheduleListRequest))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index b4351c8f2e..4c7ebc6ffa 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -1916,6 +1916,11 @@ public sealed partial class SessionStartData [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } + /// Auto routing preference selected at session creation time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Working directory and git context at session start. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] @@ -1995,6 +2000,11 @@ public sealed partial class SessionResumeData [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } + /// Auto routing preference active at resume time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Updated working directory and git context at resume time. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] @@ -7098,6 +7108,19 @@ public sealed partial class AssistantMessageServerTools public JsonElement[]? RawContentBlocks { get; set; } } +/// Hosted program that requested this client tool call. +/// Nested data type for AssistantMessageToolRequestCaller. +public sealed partial class AssistantMessageToolRequestCaller +{ + /// Provider-assigned identifier for the hosted caller. + [JsonPropertyName("callerId")] + public required string CallerId { get; set; } + + /// Kind of hosted caller that requested the client tool call. + [JsonPropertyName("type")] + public required AssistantMessageToolRequestCallerType Type { get; set; } +} + /// A tool invocation request from the assistant. /// Nested data type for AssistantMessageToolRequest. public sealed partial class AssistantMessageToolRequest @@ -7107,6 +7130,11 @@ public sealed partial class AssistantMessageToolRequest [JsonPropertyName("arguments")] public JsonElement? Arguments { get; set; } + /// Hosted program that requested this client tool call. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("caller")] + public AssistantMessageToolRequestCaller? Caller { get; set; } + /// Resolved intention summary describing what this specific call does. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("intentionSummary")] @@ -10305,6 +10333,70 @@ public sealed partial class McpAppToolCallCompleteToolMeta public McpAppToolCallCompleteToolMetaUI? Ui { get; set; } } +/// Routing preference used when the session model is `auto`. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AutoTier : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AutoTier(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Optimize for efficiency. + public static AutoTier Efficiency { get; } = new("efficiency"); + + /// Balance efficiency and intelligence. + public static AutoTier Balance { get; } = new("balance"); + + /// Optimize for intelligence. + public static AutoTier Intelligence { get; } = new("intelligence"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutoTier left, AutoTier right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutoTier left, AutoTier right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AutoTier other && Equals(other); + + /// + public bool Equals(AutoTier 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 AutoTier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AutoTier value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutoTier)); + } + } +} + /// Hosting platform type of the repository (github or ado). [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -12374,6 +12466,64 @@ public override void Write(Utf8JsonWriter writer, CitationProvider value, JsonSe } } +/// Hosted program caller type. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AssistantMessageToolRequestCallerType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AssistantMessageToolRequestCallerType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the program value. + public static AssistantMessageToolRequestCallerType Program { get; } = new("program"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AssistantMessageToolRequestCallerType left, AssistantMessageToolRequestCallerType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AssistantMessageToolRequestCallerType left, AssistantMessageToolRequestCallerType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AssistantMessageToolRequestCallerType other && Equals(other); + + /// + public bool Equals(AssistantMessageToolRequestCallerType 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 AssistantMessageToolRequestCallerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AssistantMessageToolRequestCallerType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AssistantMessageToolRequestCallerType)); + } + } +} + /// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -15438,6 +15588,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(AssistantMessageStartData))] [JsonSerializable(typeof(AssistantMessageStartEvent))] [JsonSerializable(typeof(AssistantMessageToolRequest))] +[JsonSerializable(typeof(AssistantMessageToolRequestCaller))] [JsonSerializable(typeof(AssistantReasoningData))] [JsonSerializable(typeof(AssistantReasoningDeltaData))] [JsonSerializable(typeof(AssistantReasoningDeltaEvent))] diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index edd5f925fe..706f32bf5f 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -1361,6 +1361,10 @@ type CanvasSessionContext struct { // Experimental: CapiSessionOptions is part of an experimental API and may change or be // removed. type CapiSessionOptions struct { + // Routing preference used when the session model is `auto`. The runtime persists the + // preference across cold resume. When omitted, the default routing behavior is used. + // Resuming an already-resident session cannot change its preference. + AutoTier *AutoTier `json:"autoTier,omitempty"` // Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when // the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses // transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting @@ -2599,6 +2603,8 @@ type EnqueueCommandParams struct { // Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO // with any in-flight items; if the session is idle, processing kicks off immediately. Command string `json:"command"` + // Optional user-facing text for the queue row. The command string is shown when omitted. + DisplayText *string `json:"displayText,omitempty"` } // Indicates whether the command was accepted into the local execution queue. @@ -10343,6 +10349,18 @@ type SandboxConfigUserPolicySeatbelt struct { KeychainAccess *bool `json:"keychainAccess,omitempty"` } +// Managed sandbox enforcement state for a session. +// Experimental: SandboxEnforcementStatus is part of an experimental API and may change or +// be removed. +type SandboxEnforcementStatus struct { + // Whether an enforcement failure has permanently blocked the session. + Blocked bool `json:"blocked"` + // The first sandbox enforcement failure that blocked the session. + Reason *string `json:"reason,omitempty"` + // Whether the effective managed policy requires an available sandbox backend. + Required bool `json:"required"` +} + // Register an absolute-time scheduled prompt. // Experimental: ScheduleAddAtRequest is part of an experimental API and may change or be // removed. @@ -15858,6 +15876,19 @@ const ( AuthInfoTypeUser AuthInfoType = "user" ) +// Routing preference used when the session model is `auto`. +// Experimental: AutoTier is part of an experimental API and may change or be removed. +type AutoTier string + +const ( + // Balance efficiency and intelligence. + AutoTierBalance AutoTier = "balance" + // Optimize for efficiency. + AutoTierEfficiency AutoTier = "efficiency" + // Optimize for intelligence. + AutoTierIntelligence AutoTier = "intelligence" +) + // Custom input-format kind. // Experimental: BuiltinToolFormatType is part of an experimental API and may change or be // removed. @@ -21250,6 +21281,9 @@ func (a *CommandsAPI) Enqueue(ctx context.Context, params *EnqueueCommandParams) req := map[string]any{"sessionId": a.sessionID} if params != nil { req["command"] = params.Command + if params.DisplayText != nil { + req["displayText"] = *params.DisplayText + } } raw, err := a.client.Request(ctx, "session.commands.enqueue", req) if err != nil { @@ -25037,6 +25071,28 @@ func (a *RemoteAPI) NotifySteerableChanged(ctx context.Context, params *RemoteNo return &result, nil } +// Experimental: SandboxAPI contains experimental APIs that may change or be removed. +type SandboxAPI sessionAPI + +// GetEnforcementStatus returns whether managed policy requires sandbox enforcement and +// whether an enforcement failure has permanently blocked the session. +// +// RPC method: session.sandbox.getEnforcementStatus. +// +// Returns: Managed sandbox enforcement state for a session. +func (a *SandboxAPI) GetEnforcementStatus(ctx context.Context) (*SandboxEnforcementStatus, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.sandbox.getEnforcementStatus", req) + if err != nil { + return nil, err + } + var result SandboxEnforcementStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ScheduleAPI contains experimental APIs that may change or be removed. type ScheduleAPI sessionAPI @@ -26586,6 +26642,7 @@ type SessionRPC struct { Provider *ProviderAPI Queue *QueueAPI Remote *RemoteAPI + Sandbox *SandboxAPI Schedule *ScheduleAPI Shell *ShellAPI Skills *SkillsAPI @@ -26907,6 +26964,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r.Provider = (*ProviderAPI)(&r.common) r.Queue = (*QueueAPI)(&r.common) r.Remote = (*RemoteAPI)(&r.common) + r.Sandbox = (*SandboxAPI)(&r.common) r.Schedule = (*ScheduleAPI)(&r.common) r.Shell = (*ShellAPI)(&r.common) r.Skills = (*SkillsAPI)(&r.common) diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 7f24ab6283..07504e8815 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -2128,6 +2128,8 @@ func (*SessionHandoffData) Type() SessionEventType { return SessionEventTypeSess type SessionStartData struct { // Whether the session was already in use by another client at start time AlreadyInUse *bool `json:"alreadyInUse,omitempty"` + // Auto routing preference selected at session creation time + AutoTier *AutoTier `json:"autoTier,omitempty"` // Working directory and git context at session start Context *WorkingDirectoryContext `json:"context,omitempty"` // Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) @@ -2206,6 +2208,8 @@ func (*SessionSessionLimitsChangedData) Type() SessionEventType { type SessionResumeData struct { // Whether the session was already in use by another client at resume time AlreadyInUse *bool `json:"alreadyInUse,omitempty"` + // Auto routing preference active at resume time + AutoTier *AutoTier `json:"autoTier,omitempty"` // Updated working directory and git context at resume time Context *WorkingDirectoryContext `json:"context,omitempty"` // Context tier currently selected at resume time; null when no tier is active @@ -2808,6 +2812,8 @@ type AssistantMessageServerTools struct { type AssistantMessageToolRequest struct { // Arguments to pass to the tool, format depends on the tool Arguments any `json:"arguments,omitempty"` + // Hosted program that requested this client tool call + Caller *AssistantMessageToolRequestCaller `json:"caller,omitempty"` // Resolved intention summary describing what this specific call does IntentionSummary *string `json:"intentionSummary,omitempty"` // Name of the MCP server hosting this tool, when the tool is an MCP tool @@ -2824,6 +2830,14 @@ type AssistantMessageToolRequest struct { Type *AssistantMessageToolRequestType `json:"type,omitempty"` } +// Hosted program that requested this client tool call +type AssistantMessageToolRequestCaller struct { + // Provider-assigned identifier for the hosted caller. + CallerID string `json:"callerId"` + // Kind of hosted caller that requested the client tool call. + Type AssistantMessageToolRequestCallerType `json:"type"` +} + // Per-request cost and usage data from the CAPI copilot_usage response field type AssistantUsageCopilotUsage struct { // Itemized token usage breakdown @@ -4856,6 +4870,13 @@ const ( AgentInterruptedCancelPhasePreFirstToken AgentInterruptedCancelPhase = "pre_first_token" ) +// Hosted program caller type +type AssistantMessageToolRequestCallerType string + +const ( + AssistantMessageToolRequestCallerTypeProgram AssistantMessageToolRequestCallerType = "program" +) + // Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. type AssistantMessageToolRequestType string diff --git a/go/zsession_events.go b/go/zsession_events.go index 38ea78087b..8731009064 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -23,6 +23,8 @@ type ( AssistantMessageServerTools = rpc.AssistantMessageServerTools AssistantMessageStartData = rpc.AssistantMessageStartData AssistantMessageToolRequest = rpc.AssistantMessageToolRequest + AssistantMessageToolRequestCaller = rpc.AssistantMessageToolRequestCaller + AssistantMessageToolRequestCallerType = rpc.AssistantMessageToolRequestCallerType AssistantMessageToolRequestType = rpc.AssistantMessageToolRequestType AssistantReasoningData = rpc.AssistantReasoningData AssistantReasoningDeltaData = rpc.AssistantReasoningDeltaData @@ -69,6 +71,7 @@ type ( AutoModeSwitchResponse = rpc.AutoModeSwitchResponse AutopilotObjectiveChangedOperation = rpc.AutopilotObjectiveChangedOperation AutopilotObjectiveChangedStatus = rpc.AutopilotObjectiveChangedStatus + AutoTier = rpc.AutoTier BinaryAssetReference = rpc.BinaryAssetReference BinaryAssetReferenceType = rpc.BinaryAssetReferenceType BinaryAssetType = rpc.BinaryAssetType @@ -424,6 +427,7 @@ const ( AgentInterruptedActivityToolCall = rpc.AgentInterruptedActivityToolCall AgentInterruptedCancelPhaseMidStream = rpc.AgentInterruptedCancelPhaseMidStream AgentInterruptedCancelPhasePreFirstToken = rpc.AgentInterruptedCancelPhasePreFirstToken + AssistantMessageToolRequestCallerTypeProgram = rpc.AssistantMessageToolRequestCallerTypeProgram AssistantMessageToolRequestTypeCustom = rpc.AssistantMessageToolRequestTypeCustom AssistantMessageToolRequestTypeFunction = rpc.AssistantMessageToolRequestTypeFunction AssistantUsageAPIEndpointChatCompletions = rpc.AssistantUsageAPIEndpointChatCompletions @@ -472,6 +476,9 @@ const ( AutopilotObjectiveChangedStatusCapReached = rpc.AutopilotObjectiveChangedStatusCapReached AutopilotObjectiveChangedStatusCompleted = rpc.AutopilotObjectiveChangedStatusCompleted AutopilotObjectiveChangedStatusPaused = rpc.AutopilotObjectiveChangedStatusPaused + AutoTierBalance = rpc.AutoTierBalance + AutoTierEfficiency = rpc.AutoTierEfficiency + AutoTierIntelligence = rpc.AutoTierIntelligence BinaryAssetReferenceTypeImage = rpc.BinaryAssetReferenceTypeImage BinaryAssetReferenceTypeResource = rpc.BinaryAssetReferenceTypeResource BinaryAssetTypeImage = rpc.BinaryAssetTypeImage diff --git a/java/pom.xml b/java/pom.xml index 3e5e1645a5..5b493fcc55 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -63,7 +63,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.82-0 + ^1.0.82 true diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 985ae5d5db..71abcd72dc 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.82", "json-schema": "^0.4.0", "tsx": "^4.23.12" } @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.82-0.tgz", - "integrity": "sha512-fSZVNAzFFYaS6btYD0+cKF7SrrtOklhpkPs/cIMZY7Fgxoa6rfZrlTWXlQNgNIdwLp51XxwTfxnZO+D9+MQ5yg==", + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.82.tgz", + "integrity": "sha512-+mDIwBO3dCpL3k2rVLc4+1tFzqcVBTJNA/0co+okEpmCgcHjaz91Cqq7++pJKN5yJQuGC6bKangm7PSoheI1Xw==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.82-0", - "@github/copilot-darwin-x64": "1.0.82-0", - "@github/copilot-linux-arm64": "1.0.82-0", - "@github/copilot-linux-x64": "1.0.82-0", - "@github/copilot-linuxmusl-arm64": "1.0.82-0", - "@github/copilot-linuxmusl-x64": "1.0.82-0", - "@github/copilot-win32-arm64": "1.0.82-0", - "@github/copilot-win32-x64": "1.0.82-0" + "@github/copilot-darwin-arm64": "1.0.82", + "@github/copilot-darwin-x64": "1.0.82", + "@github/copilot-linux-arm64": "1.0.82", + "@github/copilot-linux-x64": "1.0.82", + "@github/copilot-linuxmusl-arm64": "1.0.82", + "@github/copilot-linuxmusl-x64": "1.0.82", + "@github/copilot-win32-arm64": "1.0.82", + "@github/copilot-win32-x64": "1.0.82" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.82-0.tgz", - "integrity": "sha512-TzBYfyvxcw3z9Mu7U8TsFo/Nq7m5XS6ahT71aPL+gx/YId0kmenI27b9daXsK6LA1D0gsFGwNBDKINddqntt1g==", + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.82.tgz", + "integrity": "sha512-UpVSFA0COmlIakAr7/6WJqJlabtKgY8y5en6La3IxGxXsLlbkGoeevSqyfXvqJyHxaGn0YGpOC1oEPL379JfCw==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.82-0.tgz", - "integrity": "sha512-Lm/U5Q8kN8yEeBTWKTfIxXAgXaT6zqdBzAAO7lA4DWHZ92AEeZZiVTs6jEWsQ2aWB2uMs6zbn0vi6t+/jIqCGw==", + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.82.tgz", + "integrity": "sha512-wMKbxK8fpKsbATjn9dE31Pae67H1xu6dmaCuyXpXjXLsNPVjeLFszJZ30MAOwxRWz4dmA8VvEJZmLgJekojo2Q==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.82-0.tgz", - "integrity": "sha512-YERVMC1Q4p6l6KQHL5rVOI52rWbvgp9IwyzUBaVSGrfFuqu5BEvZ9bgHPsxTYi3Npkt5KVOXEyPMU5rAY09qQQ==", + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.82.tgz", + "integrity": "sha512-YDQmh0F+GzWORPmNNxQdcjHcXHxcy08dhMhbAhu4cqtMeA2sZWQqhfk9mJhHppMm9U0uR7h0KAB6nsgw/8Bsuw==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.82-0.tgz", - "integrity": "sha512-z2hxMVjqt4+xDRFTZv3/0K3X+aqcJhd6zPO2JxCpOVTh5CNZFaWk+XIa2iXAPWxFqdKJsQ4muXMl3zInaAOkRw==", + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.82.tgz", + "integrity": "sha512-vqmG9665ktgLHAkGKMjp4uVMdHqLxi8uS9zL+g2pMwZUPoM7JxkaSfOoeyYr99caF7J6VIJTWv4LdRNQyG5jrQ==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.82-0.tgz", - "integrity": "sha512-EcUCv2PKhBzCCvpTaS511VYTDWyhudyIRPvBpc9gFNO3hjlgiNDusf4k9vP6+E3/lHenwGYzMsyRHcYIOy4vcQ==", + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.82.tgz", + "integrity": "sha512-14dWfa4rBME55bKmF+Z8V+WzZNgPQZKFFBQX+EOiAgLVV/arQCnQ1m7wwa5oXjQeCIgkS/L9J8uM8jNm6cZbOA==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.82-0.tgz", - "integrity": "sha512-1fKVjUiZ1tdb0/d/re90EpFGXhlIPfjENp2Wo/2Kj592dWO3+IwM2qV/AOdMQ4pacW5iYHII7nibx1/EYq3LGQ==", + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.82.tgz", + "integrity": "sha512-Mauqa2TBjtB8W/1KXF/jJ4MbtwnLvoEQNHYoSyoX+s+nIpttmixmYBJDwKV89ZlQRdjOCuxOgraRLQ9hjjXvNQ==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.82-0.tgz", - "integrity": "sha512-H341wuxQHhwe/yLmORHzwC3DGzFZgGzh+TpwfyK2zjeYVbwDZ3Bax8M+9CzIED9jIBdEfAmfGzxKUHFugpXTtQ==", + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.82.tgz", + "integrity": "sha512-PaW9s0GTgM+svtDkB583dZRrhLrO4o10y2ozMmQ0Hi5eB11C24UdC5U81nFlvKPED7+GlnBJIFQ+oHaxHrnp3A==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.82-0.tgz", - "integrity": "sha512-f1ba3gG8NaoYWFHtHaHcLN4It7mclkWdCOXvwFPqPEwqCEIx/+Zh6VHiOeIcNWRS0elRP6QYDCKaTDy1TW27uQ==", + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.82.tgz", + "integrity": "sha512-m4iSROOMEPp1yRIQQwbnO0CDfwB4syESyHoeSLLFuynMR4/ApghAdyBrBQQcTKGsUu3R5rOE64RYlVmyKmuA9A==", "cpu": [ "x64" ], diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index f5d6bf66da..1b33a27904 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.82", "json-schema": "^0.4.0", "tsx": "^4.23.12" } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequest.java index 2013734012..bcc7c8206d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequest.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequest.java @@ -36,6 +36,8 @@ public record AssistantMessageToolRequest( /** Original tool name on the MCP server, when the tool is an MCP tool */ @JsonProperty("mcpToolName") String mcpToolName, /** Resolved intention summary describing what this specific call does */ - @JsonProperty("intentionSummary") String intentionSummary + @JsonProperty("intentionSummary") String intentionSummary, + /** Hosted program that requested this client tool call */ + @JsonProperty("caller") AssistantMessageToolRequestCaller caller ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCaller.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCaller.java new file mode 100644 index 0000000000..cb3d05a649 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCaller.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; + +/** + * Hosted program that requested this client tool call + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AssistantMessageToolRequestCaller( + /** Kind of hosted caller that requested the client tool call. */ + @JsonProperty("type") AssistantMessageToolRequestCallerType type, + /** Provider-assigned identifier for the hosted caller. */ + @JsonProperty("callerId") String callerId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCallerType.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCallerType.java new file mode 100644 index 0000000000..05be5068d9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCallerType.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Hosted program caller type + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AssistantMessageToolRequestCallerType { + /** The {@code program} variant. */ + PROGRAM("program"); + + private final String value; + AssistantMessageToolRequestCallerType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AssistantMessageToolRequestCallerType fromValue(String value) { + for (AssistantMessageToolRequestCallerType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AssistantMessageToolRequestCallerType value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AutoTier.java b/java/sdk/src/generated/java/com/github/copilot/generated/AutoTier.java new file mode 100644 index 0000000000..254543160a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AutoTier.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; + +/** + * Routing preference used when the session model is `auto`. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutoTier { + /** The {@code efficiency} variant. */ + EFFICIENCY("efficiency"), + /** The {@code balance} variant. */ + BALANCE("balance"), + /** The {@code intelligence} variant. */ + INTELLIGENCE("intelligence"); + + private final String value; + AutoTier(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AutoTier fromValue(String value) { + for (AutoTier v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutoTier value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java index a3f39d7696..de54f8a3ce 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java @@ -51,6 +51,8 @@ public record SessionResumeEventData( @JsonProperty("verbosity") Verbosity verbosity, /** Context tier currently selected at resume time; null when no tier is active */ @JsonProperty("contextTier") ContextTier contextTier, + /** Auto routing preference active at resume time */ + @JsonProperty("autoTier") AutoTier autoTier, /** Session limits currently configured at resume time; null when no limits are active */ @JsonProperty("sessionLimits") SessionLimitsConfig sessionLimits, /** Updated working directory and git context at resume time */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java index bf8b4e91cf..b977ae036f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java @@ -55,6 +55,8 @@ public record SessionStartEventData( @JsonProperty("verbosity") Verbosity verbosity, /** 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, + /** Auto routing preference selected at session creation time */ + @JsonProperty("autoTier") AutoTier autoTier, /** Session limits configured at session creation time, if any */ @JsonProperty("sessionLimits") SessionLimitsConfig sessionLimits, /** Working directory and git context at session start */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTier.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTier.java new file mode 100644 index 0000000000..a4433e1ea9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTier.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Routing preference used when the session model is `auto`. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutoTier { + /** The {@code efficiency} variant. */ + EFFICIENCY("efficiency"), + /** The {@code balance} variant. */ + BALANCE("balance"), + /** The {@code intelligence} variant. */ + INTELLIGENCE("intelligence"); + + private final String value; + AutoTier(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AutoTier fromValue(String value) { + for (AutoTier v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutoTier value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java index 27fd29128d..e77117b2cb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java @@ -21,6 +21,8 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record CapiSessionOptions( + /** Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. */ + @JsonProperty("autoTier") AutoTier autoTier, /** Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. */ @JsonProperty("enableWebSocketResponses") Boolean enableWebSocketResponses ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsEnqueueParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsEnqueueParams.java index d7725bc9cf..073038d2fc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsEnqueueParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsEnqueueParams.java @@ -27,6 +27,8 @@ public record SessionCommandsEnqueueParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. */ - @JsonProperty("command") String command + @JsonProperty("command") String command, + /** Optional user-facing text for the queue row. The command string is shown when omitted. */ + @JsonProperty("displayText") String displayText ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java index 05cfd396d4..b9bd6b8226 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java @@ -29,6 +29,8 @@ public final class SessionRpc { private final RpcCaller caller; private final String sessionId; + /** API methods for the {@code sandbox} namespace. */ + public final SessionSandboxApi sandbox; /** API methods for the {@code gitHubAuth} namespace. */ public final SessionGitHubAuthApi gitHubAuth; /** API methods for the {@code debug} namespace. */ @@ -115,6 +117,7 @@ public final class SessionRpc { public SessionRpc(RpcCaller caller, String sessionId) { this.caller = caller; this.sessionId = sessionId; + this.sandbox = new SessionSandboxApi(caller, sessionId); this.gitHubAuth = new SessionGitHubAuthApi(caller, sessionId); this.debug = new SessionDebugApi(caller, sessionId); this.canvas = new SessionCanvasApi(caller, sessionId); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java new file mode 100644 index 0000000000..55efb9da78 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sandbox} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionSandboxApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionSandboxApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getEnforcementStatus() { + return caller.invoke("session.sandbox.getEnforcementStatus", java.util.Map.of("sessionId", this.sessionId), SessionSandboxGetEnforcementStatusResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusParams.java new file mode 100644 index 0000000000..ec92981e04 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusParams.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; + +/** + * Identifies the target session. + * + * @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 SessionSandboxGetEnforcementStatusParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusResult.java new file mode 100644 index 0000000000..c1a0e77a1a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusResult.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; + +/** + * Managed sandbox enforcement state for a session. + * + * @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 SessionSandboxGetEnforcementStatusResult( + /** Whether the effective managed policy requires an available sandbox backend. */ + @JsonProperty("required") Boolean required, + /** Whether an enforcement failure has permanently blocked the session. */ + @JsonProperty("blocked") Boolean blocked, + /** The first sandbox enforcement failure that blocked the session. */ + @JsonProperty("reason") String reason +) { +} diff --git a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java index 529c42f2bb..b75e710720 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java @@ -213,7 +213,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, null, null, null, null, null, null)); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -890,7 +890,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, null, null, null, null, null, null); event.setData(data); return event; } diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 3b8e6cb861..6cefab6031 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.82", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -658,8 +658,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.82-0", - "integrity": "sha512-fSZVNAzFFYaS6btYD0+cKF7SrrtOklhpkPs/cIMZY7Fgxoa6rfZrlTWXlQNgNIdwLp51XxwTfxnZO+D9+MQ5yg==", + "version": "1.0.82", + "integrity": "sha512-+mDIwBO3dCpL3k2rVLc4+1tFzqcVBTJNA/0co+okEpmCgcHjaz91Cqq7++pJKN5yJQuGC6bKangm7PSoheI1Xw==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -668,19 +668,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.82-0", - "@github/copilot-darwin-x64": "1.0.82-0", - "@github/copilot-linux-arm64": "1.0.82-0", - "@github/copilot-linux-x64": "1.0.82-0", - "@github/copilot-linuxmusl-arm64": "1.0.82-0", - "@github/copilot-linuxmusl-x64": "1.0.82-0", - "@github/copilot-win32-arm64": "1.0.82-0", - "@github/copilot-win32-x64": "1.0.82-0" + "@github/copilot-darwin-arm64": "1.0.82", + "@github/copilot-darwin-x64": "1.0.82", + "@github/copilot-linux-arm64": "1.0.82", + "@github/copilot-linux-x64": "1.0.82", + "@github/copilot-linuxmusl-arm64": "1.0.82", + "@github/copilot-linuxmusl-x64": "1.0.82", + "@github/copilot-win32-arm64": "1.0.82", + "@github/copilot-win32-x64": "1.0.82" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-TzBYfyvxcw3z9Mu7U8TsFo/Nq7m5XS6ahT71aPL+gx/YId0kmenI27b9daXsK6LA1D0gsFGwNBDKINddqntt1g==", + "version": "1.0.82", + "integrity": "sha512-UpVSFA0COmlIakAr7/6WJqJlabtKgY8y5en6La3IxGxXsLlbkGoeevSqyfXvqJyHxaGn0YGpOC1oEPL379JfCw==", "cpu": [ "arm64" ], @@ -694,8 +694,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.82-0", - "integrity": "sha512-Lm/U5Q8kN8yEeBTWKTfIxXAgXaT6zqdBzAAO7lA4DWHZ92AEeZZiVTs6jEWsQ2aWB2uMs6zbn0vi6t+/jIqCGw==", + "version": "1.0.82", + "integrity": "sha512-wMKbxK8fpKsbATjn9dE31Pae67H1xu6dmaCuyXpXjXLsNPVjeLFszJZ30MAOwxRWz4dmA8VvEJZmLgJekojo2Q==", "cpu": [ "x64" ], @@ -709,8 +709,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-YERVMC1Q4p6l6KQHL5rVOI52rWbvgp9IwyzUBaVSGrfFuqu5BEvZ9bgHPsxTYi3Npkt5KVOXEyPMU5rAY09qQQ==", + "version": "1.0.82", + "integrity": "sha512-YDQmh0F+GzWORPmNNxQdcjHcXHxcy08dhMhbAhu4cqtMeA2sZWQqhfk9mJhHppMm9U0uR7h0KAB6nsgw/8Bsuw==", "cpu": [ "arm64" ], @@ -724,8 +724,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.82-0", - "integrity": "sha512-z2hxMVjqt4+xDRFTZv3/0K3X+aqcJhd6zPO2JxCpOVTh5CNZFaWk+XIa2iXAPWxFqdKJsQ4muXMl3zInaAOkRw==", + "version": "1.0.82", + "integrity": "sha512-vqmG9665ktgLHAkGKMjp4uVMdHqLxi8uS9zL+g2pMwZUPoM7JxkaSfOoeyYr99caF7J6VIJTWv4LdRNQyG5jrQ==", "cpu": [ "x64" ], @@ -739,8 +739,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-EcUCv2PKhBzCCvpTaS511VYTDWyhudyIRPvBpc9gFNO3hjlgiNDusf4k9vP6+E3/lHenwGYzMsyRHcYIOy4vcQ==", + "version": "1.0.82", + "integrity": "sha512-14dWfa4rBME55bKmF+Z8V+WzZNgPQZKFFBQX+EOiAgLVV/arQCnQ1m7wwa5oXjQeCIgkS/L9J8uM8jNm6cZbOA==", "cpu": [ "arm64" ], @@ -754,8 +754,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.82-0", - "integrity": "sha512-1fKVjUiZ1tdb0/d/re90EpFGXhlIPfjENp2Wo/2Kj592dWO3+IwM2qV/AOdMQ4pacW5iYHII7nibx1/EYq3LGQ==", + "version": "1.0.82", + "integrity": "sha512-Mauqa2TBjtB8W/1KXF/jJ4MbtwnLvoEQNHYoSyoX+s+nIpttmixmYBJDwKV89ZlQRdjOCuxOgraRLQ9hjjXvNQ==", "cpu": [ "x64" ], @@ -769,8 +769,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-H341wuxQHhwe/yLmORHzwC3DGzFZgGzh+TpwfyK2zjeYVbwDZ3Bax8M+9CzIED9jIBdEfAmfGzxKUHFugpXTtQ==", + "version": "1.0.82", + "integrity": "sha512-PaW9s0GTgM+svtDkB583dZRrhLrO4o10y2ozMmQ0Hi5eB11C24UdC5U81nFlvKPED7+GlnBJIFQ+oHaxHrnp3A==", "cpu": [ "arm64" ], @@ -784,8 +784,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.82-0", - "integrity": "sha512-f1ba3gG8NaoYWFHtHaHcLN4It7mclkWdCOXvwFPqPEwqCEIx/+Zh6VHiOeIcNWRS0elRP6QYDCKaTDy1TW27uQ==", + "version": "1.0.82", + "integrity": "sha512-m4iSROOMEPp1yRIQQwbnO0CDfwB4syESyHoeSLLFuynMR4/ApghAdyBrBQQcTKGsUu3R5rOE64RYlVmyKmuA9A==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 89863520e0..ff54fb3682 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.82", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index ad675a88c7..16a17c7d21 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.82", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index db0ea63dcc..024dd3a5bb 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, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; +import type { AbortReason, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; /** A value that can be represented losslessly on the SDK JSON wire. */ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; @@ -5487,6 +5487,7 @@ export interface CanvasProviderUnregisterRequest { */ /** @experimental */ export interface CapiSessionOptions { + autoTier?: AutoTier; /** * Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. */ @@ -6859,6 +6860,10 @@ export interface EnqueueCommandParams { * Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. */ command: string; + /** + * Optional user-facing text for the queue row. The command string is shown when omitted. + */ + displayText?: string | null; } /** * Indicates whether the command was accepted into the local execution queue. @@ -16588,6 +16593,27 @@ export interface SandboxConfigAuth { */ gh?: boolean; } +/** + * Managed sandbox enforcement state for a session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxEnforcementStatus". + */ +/** @experimental */ +export interface SandboxEnforcementStatus { + /** + * Whether the effective managed policy requires an available sandbox backend. + */ + required: boolean; + /** + * Whether an enforcement failure has permanently blocked the session. + */ + blocked: boolean; + /** + * The first sandbox enforcement failure that blocked the session. + */ + reason?: string; +} /** * Register an absolute-time scheduled prompt. * @@ -23625,6 +23651,16 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ sendMessages: async (params: SendMessagesRequest): Promise => connection.sendRequest("session.sendMessages", { sessionId, ...params }), + /** @experimental */ + sandbox: { + /** + * Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. + * + * @returns Managed sandbox enforcement state for a session. + */ + getEnforcementStatus: async (): Promise => + connection.sendRequest("session.sandbox.getEnforcementStatus", { sessionId }), + }, /** * Aborts the current agent turn. * diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 9075379982..cb845443aa 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -135,6 +135,16 @@ export type SessionEvent = | CanvasRemovedEvent | ExtensionsAttachmentsPushedEvent | McpAppToolCallCompleteEvent; +/** + * Routing preference used when the session model is `auto`. + */ +export type AutoTier = + /** Optimize for efficiency. */ + | "efficiency" + /** Balance efficiency and intelligence. */ + | "balance" + /** Optimize for intelligence. */ + | "intelligence"; /** * Hosting platform type of the repository (github or ado) */ @@ -469,6 +479,10 @@ export type CitationProvider = */ /** @experimental */ export type CitationLocation = CitationLocationChar | CitationLocationPage | CitationLocationBlock; +/** + * Hosted program caller type + */ +export type AssistantMessageToolRequestCallerType = "program"; /** * API endpoint used for this model call, matching CAPI supported_endpoints vocabulary */ @@ -1106,6 +1120,7 @@ export interface StartData { * Whether the session was already in use by another client at start time */ alreadyInUse?: boolean; + autoTier?: AutoTier; context?: WorkingDirectoryContext; /** * Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) @@ -1258,6 +1273,7 @@ export interface ResumeData { * Whether the session was already in use by another client at resume time */ alreadyInUse?: boolean; + autoTier?: AutoTier; context?: WorkingDirectoryContext; /** * Context tier currently selected at resume time; null when no tier is active @@ -4843,6 +4859,7 @@ export interface AssistantMessageToolRequest { * Arguments to pass to the tool, format depends on the tool */ arguments?: JsonValue; + caller?: AssistantMessageToolRequestCaller; /** * Resolved intention summary describing what this specific call does */ @@ -4869,6 +4886,16 @@ export interface AssistantMessageToolRequest { toolTitle?: string; type?: AssistantMessageToolRequestType; } +/** + * Hosted program that requested this client tool call + */ +export interface AssistantMessageToolRequestCaller { + /** + * Provider-assigned identifier for the hosted caller. + */ + callerId: string; + type: AssistantMessageToolRequestCallerType; +} /** * Session event "assistant.message_start". Streaming assistant message start metadata */ diff --git a/nodejs/test/session-event-codegen.test.ts b/nodejs/test/session-event-codegen.test.ts index c84d9bc082..2dad8f4f18 100644 --- a/nodejs/test/session-event-codegen.test.ts +++ b/nodejs/test/session-event-codegen.test.ts @@ -3,10 +3,37 @@ import { describe, expect, it } from "vitest"; import { generateSessionEventsCode as generateCSharpSessionEventsCode } from "../../scripts/codegen/csharp.ts"; import { generateGoSessionEventsCode } from "../../scripts/codegen/go.ts"; -import { generatePythonSessionEventsCode } from "../../scripts/codegen/python.ts"; +import { + generatePythonSessionEventsCode, + postProcessExternalRefsForPython, +} from "../../scripts/codegen/python.ts"; import { generateSessionEventsCode as generateRustSessionEventsCode } from "../../scripts/codegen/rust.ts"; describe("session event codegen", () => { + it("replaces external reference placeholders regardless of acronym casing", () => { + const code = `@dataclass +class ExternalRefMCPOauthHTTPResponse: + external_ref_marker_external_ref_mcp_oauth_http_response: str + + @staticmethod + def from_dict(obj: Any) -> 'ExternalRefMCPOauthHTTPResponse': + value = obj.get("__externalRefMarker___ExternalRef_McpOauthHttpResponse") + return ExternalRefMCPOauthHTTPResponse(value) + +@dataclass +class ProbeResult: + response: ExternalRefMCPOauthHTTPResponse +`; + + const processed = postProcessExternalRefsForPython( + code, + new Map([["__ExternalRef_McpOauthHttpResponse", "McpOauthHttpResponse"]]) + ); + + expect(processed).toContain("response: McpOauthHttpResponse"); + expect(processed).not.toContain("class ExternalRefMCPOauthHTTPResponse"); + }); + it("maps special schema formats to the expected Python types", () => { const schema: JSONSchema7 = { definitions: { diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 1d59a55f4a..84f3718b16 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, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity +from .session_events import AbortReason, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity if TYPE_CHECKING: from .._jsonrpc import JsonRpcClient @@ -1051,6 +1051,11 @@ def to_dict(self) -> dict: class CapiSessionOptions: """Options scoped to the built-in CAPI (Copilot API) provider.""" + auto_tier: AutoTier | None = None + """Routing preference used when the session model is `auto`. The runtime persists the + preference across cold resume. When omitted, the default routing behavior is used. + Resuming an already-resident session cannot change its preference. + """ enable_web_socket_responses: bool | None = None """Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses @@ -1062,11 +1067,14 @@ class CapiSessionOptions: @staticmethod def from_dict(obj: Any) -> 'CapiSessionOptions': assert isinstance(obj, dict) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) enable_web_socket_responses = from_union([from_bool, from_none], obj.get("enableWebSocketResponses")) - return CapiSessionOptions(enable_web_socket_responses) + return CapiSessionOptions(auto_tier, enable_web_socket_responses) def to_dict(self) -> dict: result: dict = {} + if self.auto_tier is not None: + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) if self.enable_web_socket_responses is not None: result["enableWebSocketResponses"] = from_union([from_bool, from_none], self.enable_web_socket_responses) return result @@ -2502,16 +2510,21 @@ class EnqueueCommandParams: """Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. """ + display_text: str | None = None + """Optional user-facing text for the queue row. The command string is shown when omitted.""" @staticmethod def from_dict(obj: Any) -> 'EnqueueCommandParams': assert isinstance(obj, dict) command = from_str(obj.get("command")) - return EnqueueCommandParams(command) + display_text = from_union([from_none, from_str], obj.get("displayText")) + return EnqueueCommandParams(command, display_text) def to_dict(self) -> dict: result: dict = {} result["command"] = from_str(self.command) + if self.display_text is not None: + result["displayText"] = from_union([from_none, from_str], self.display_text) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -5576,28 +5589,6 @@ def to_dict(self) -> dict: result["serverName"] = from_str(self.server_name) return result -@dataclass -class ExternalRefMCPOauthHTTPResponse: - """HTTP response returned by the server. - - HTTP 401 or 403 response returned by the server. - - HTTP response returned by the server, when the probe reached the server and captured the - complete response. - """ - external_ref_marker_external_ref_mcp_oauth_http_response: str - - @staticmethod - def from_dict(obj: Any) -> 'ExternalRefMCPOauthHTTPResponse': - assert isinstance(obj, dict) - external_ref_marker_external_ref_mcp_oauth_http_response = from_str(obj.get("__externalRefMarker___ExternalRef_McpOauthHttpResponse")) - return ExternalRefMCPOauthHTTPResponse(external_ref_marker_external_ref_mcp_oauth_http_response) - - def to_dict(self) -> dict: - result: dict = {} - result["__externalRefMarker___ExternalRef_McpOauthHttpResponse"] = from_str(self.external_ref_marker_external_ref_mcp_oauth_http_response) - return result - class Status(Enum): AUTHENTICATED = "authenticated" FAILED = "failed" @@ -9664,6 +9655,36 @@ class _SandboxConfigSource(Enum): USER_DISABLED = "user_disabled" USER_ENABLED = "user_enabled" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxEnforcementStatus: + """Managed sandbox enforcement state for a session.""" + + blocked: bool + """Whether an enforcement failure has permanently blocked the session.""" + + required: bool + """Whether the effective managed policy requires an available sandbox backend.""" + + reason: str | None = None + """The first sandbox enforcement failure that blocked the session.""" + + @staticmethod + def from_dict(obj: Any) -> 'SandboxEnforcementStatus': + assert isinstance(obj, dict) + blocked = from_bool(obj.get("blocked")) + required = from_bool(obj.get("required")) + reason = from_union([from_str, from_none], obj.get("reason")) + return SandboxEnforcementStatus(blocked, required, reason) + + def to_dict(self) -> dict: + result: dict = {} + result["blocked"] = from_bool(self.blocked) + result["required"] = from_bool(self.required) + if self.reason is not None: + result["reason"] = from_union([from_str, from_none], self.reason) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ScheduleAddAtRequest: @@ -32942,7 +32963,7 @@ class MCPOauthProbeResult: status: Status """Probe outcome variant discriminator.""" - http_response: ExternalRefMCPOauthHTTPResponse | None = None + http_response: McpOauthHttpResponse | None = None """HTTP response returned by the server. HTTP 401 or 403 response returned by the server. @@ -32963,7 +32984,7 @@ class MCPOauthProbeResult: def from_dict(obj: Any) -> 'MCPOauthProbeResult': assert isinstance(obj, dict) status = Status(obj.get("status")) - http_response = from_union([ExternalRefMCPOauthHTTPResponse.from_dict, from_none], obj.get("httpResponse")) + http_response = from_union([McpOauthHttpResponse.from_dict, from_none], obj.get("httpResponse")) reason = from_union([MCPOauthProbeNeedsAuthReason, from_none], obj.get("reason")) www_authenticate_params = from_union([McpOauthWWWAuthenticateParams.from_dict, from_none], obj.get("wwwAuthenticateParams")) error = from_union([from_str, from_none], obj.get("error")) @@ -32973,7 +32994,7 @@ def to_dict(self) -> dict: result: dict = {} result["status"] = to_enum(Status, self.status) if self.http_response is not None: - result["httpResponse"] = from_union([lambda x: to_class(ExternalRefMCPOauthHTTPResponse, x), from_none], self.http_response) + result["httpResponse"] = from_union([lambda x: to_class(McpOauthHttpResponse, x), from_none], self.http_response) if self.reason is not None: result["reason"] = from_union([lambda x: to_enum(MCPOauthProbeNeedsAuthReason, x), from_none], self.reason) if self.www_authenticate_params is not None: @@ -35849,6 +35870,7 @@ class RPC: sandbox_config_user_policy_network: SandboxConfigUserPolicyNetwork sandbox_config_user_policy_network_proxy: SandboxConfigUserPolicyNetworkProxy sandbox_config_user_policy_seatbelt: SandboxConfigUserPolicySeatbelt + sandbox_enforcement_status: SandboxEnforcementStatus schedule_add_at_request: ScheduleAddAtRequest schedule_add_cron_request: ScheduleAddCronRequest schedule_add_request: ScheduleAddRequest @@ -37032,6 +37054,7 @@ def from_dict(obj: Any) -> 'RPC': sandbox_config_user_policy_network = SandboxConfigUserPolicyNetwork.from_dict(obj.get("SandboxConfigUserPolicyNetwork")) sandbox_config_user_policy_network_proxy = SandboxConfigUserPolicyNetworkProxy.from_dict(obj.get("SandboxConfigUserPolicyNetworkProxy")) sandbox_config_user_policy_seatbelt = SandboxConfigUserPolicySeatbelt.from_dict(obj.get("SandboxConfigUserPolicySeatbelt")) + sandbox_enforcement_status = SandboxEnforcementStatus.from_dict(obj.get("SandboxEnforcementStatus")) schedule_add_at_request = ScheduleAddAtRequest.from_dict(obj.get("ScheduleAddAtRequest")) schedule_add_cron_request = ScheduleAddCronRequest.from_dict(obj.get("ScheduleAddCronRequest")) schedule_add_request = ScheduleAddRequest.from_dict(obj.get("ScheduleAddRequest")) @@ -37410,7 +37433,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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, 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, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, 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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, 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, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -38215,6 +38238,7 @@ def to_dict(self) -> dict: result["SandboxConfigUserPolicyNetwork"] = to_class(SandboxConfigUserPolicyNetwork, self.sandbox_config_user_policy_network) result["SandboxConfigUserPolicyNetworkProxy"] = to_class(SandboxConfigUserPolicyNetworkProxy, self.sandbox_config_user_policy_network_proxy) result["SandboxConfigUserPolicySeatbelt"] = to_class(SandboxConfigUserPolicySeatbelt, self.sandbox_config_user_policy_seatbelt) + result["SandboxEnforcementStatus"] = to_class(SandboxEnforcementStatus, self.sandbox_enforcement_status) result["ScheduleAddAtRequest"] = to_class(ScheduleAddAtRequest, self.schedule_add_at_request) result["ScheduleAddCronRequest"] = to_class(ScheduleAddCronRequest, self.schedule_add_cron_request) result["ScheduleAddRequest"] = to_class(ScheduleAddRequest, self.schedule_add_request) @@ -39663,6 +39687,17 @@ async def _connect(self, params: _ConnectRequest, *, timeout: float | None = Non return _ConnectResult.from_dict(await self._client.request("connect", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class SandboxApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def get_enforcement_status(self, *, timeout: float | None = None) -> SandboxEnforcementStatus: + "Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session.\n\nReturns:\n Managed sandbox enforcement state for a session." + return SandboxEnforcementStatus.from_dict(await self._client.request("session.sandbox.getEnforcementStatus", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class GitHubAuthApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -41139,6 +41174,7 @@ class SessionRpc: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id + self.sandbox = SandboxApi(client, session_id) self.git_hub_auth = GitHubAuthApi(client, session_id) self.debug = DebugApi(client, session_id) self.canvas = CanvasApi(client, session_id) @@ -42061,7 +42097,6 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ExtensionsApi", "ExtensionsDisableRequest", "ExtensionsEnableRequest", - "ExternalRefMCPOauthHTTPResponse", "ExternalToolResult", "ExternalToolTextResultForLlm", "ExternalToolTextResultForLlmBinaryResultsForLlm", @@ -42766,6 +42801,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "RemoteSessionMode", "RemoteSessionRepository", "RunOptions", + "SandboxApi", "SandboxConfig", "SandboxConfigAuth", "SandboxConfigUserPolicy", @@ -42775,6 +42811,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SandboxConfigUserPolicyNetwork", "SandboxConfigUserPolicyNetworkProxy", "SandboxConfigUserPolicySeatbelt", + "SandboxEnforcementStatus", "Saved", "ScheduleAddAtRequest", "ScheduleAddCronRequest", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index c22f9f24fc..fd41b6c6c6 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -2450,6 +2450,7 @@ class AssistantMessageToolRequest: name: str tool_call_id: str arguments: Any = None + caller: AssistantMessageToolRequestCaller | None = None intention_summary: str | None = None mcp_server_name: str | None = None mcp_tool_name: str | None = None @@ -2462,6 +2463,7 @@ def from_dict(obj: Any) -> "AssistantMessageToolRequest": name = from_str(obj.get("name")) tool_call_id = from_str(obj.get("toolCallId")) arguments = obj.get("arguments") + caller = from_union([from_none, AssistantMessageToolRequestCaller.from_dict], obj.get("caller")) intention_summary = from_union([from_none, from_str], obj.get("intentionSummary")) mcp_server_name = from_union([from_none, from_str], obj.get("mcpServerName")) mcp_tool_name = from_union([from_none, from_str], obj.get("mcpToolName")) @@ -2471,6 +2473,7 @@ def from_dict(obj: Any) -> "AssistantMessageToolRequest": name=name, tool_call_id=tool_call_id, arguments=arguments, + caller=caller, intention_summary=intention_summary, mcp_server_name=mcp_server_name, mcp_tool_name=mcp_tool_name, @@ -2484,6 +2487,8 @@ def to_dict(self) -> dict: result["toolCallId"] = from_str(self.tool_call_id) if self.arguments is not None: result["arguments"] = self.arguments + if self.caller is not None: + result["caller"] = from_union([from_none, lambda x: to_class(AssistantMessageToolRequestCaller, x)], self.caller) if self.intention_summary is not None: result["intentionSummary"] = from_union([from_none, from_str], self.intention_summary) if self.mcp_server_name is not None: @@ -2497,6 +2502,29 @@ def to_dict(self) -> dict: return result +@dataclass +class AssistantMessageToolRequestCaller: + "Hosted program that requested this client tool call" + caller_id: str + type: AssistantMessageToolRequestCallerType + + @staticmethod + def from_dict(obj: Any) -> "AssistantMessageToolRequestCaller": + assert isinstance(obj, dict) + caller_id = from_str(obj.get("callerId")) + type = parse_enum(AssistantMessageToolRequestCallerType, obj.get("type")) + return AssistantMessageToolRequestCaller( + caller_id=caller_id, + type=type, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["callerId"] = from_str(self.caller_id) + result["type"] = to_enum(AssistantMessageToolRequestCallerType, self.type) + return result + + @dataclass class AssistantReasoningData: "Assistant reasoning content for timeline display with complete thinking text" @@ -8222,6 +8250,7 @@ class SessionResumeData: event_count: int resume_time: datetime already_in_use: bool | None = None + auto_tier: AutoTier | None = None context: WorkingDirectoryContext | None = None context_tier: ContextTier | None = None continue_pending_work: bool | None = None @@ -8240,6 +8269,7 @@ def from_dict(obj: Any) -> "SessionResumeData": event_count = from_int(obj.get("eventCount")) resume_time = from_datetime(obj.get("resumeTime")) already_in_use = from_union([from_none, from_bool], obj.get("alreadyInUse")) + auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("autoTier")) context = from_union([from_none, WorkingDirectoryContext.from_dict], obj.get("context")) context_tier = from_union([from_none, lambda x: parse_enum(ContextTier, x)], obj.get("contextTier")) continue_pending_work = from_union([from_none, from_bool], obj.get("continuePendingWork")) @@ -8255,6 +8285,7 @@ def from_dict(obj: Any) -> "SessionResumeData": event_count=event_count, resume_time=resume_time, already_in_use=already_in_use, + auto_tier=auto_tier, context=context, context_tier=context_tier, continue_pending_work=continue_pending_work, @@ -8274,6 +8305,8 @@ def to_dict(self) -> dict: result["resumeTime"] = to_datetime(self.resume_time) if self.already_in_use is not None: result["alreadyInUse"] = from_union([from_none, from_bool], self.already_in_use) + if self.auto_tier is not None: + result["autoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.auto_tier) if self.context is not None: result["context"] = from_union([from_none, lambda x: to_class(WorkingDirectoryContext, x)], self.context) if self.context_tier is not None: @@ -8566,6 +8599,7 @@ class SessionStartData: start_time: datetime version: int already_in_use: bool | None = None + auto_tier: AutoTier | None = None context: WorkingDirectoryContext | None = None context_tier: ContextTier | None = None detached_from_spawning_parent_session_id: str | None = None @@ -8586,6 +8620,7 @@ def from_dict(obj: Any) -> "SessionStartData": start_time = from_datetime(obj.get("startTime")) version = from_int(obj.get("version")) already_in_use = from_union([from_none, from_bool], obj.get("alreadyInUse")) + auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("autoTier")) context = from_union([from_none, WorkingDirectoryContext.from_dict], obj.get("context")) context_tier = from_union([from_none, lambda x: parse_enum(ContextTier, x)], obj.get("contextTier")) detached_from_spawning_parent_session_id = from_union([from_none, from_str], obj.get("detachedFromSpawningParentSessionId")) @@ -8603,6 +8638,7 @@ def from_dict(obj: Any) -> "SessionStartData": start_time=start_time, version=version, already_in_use=already_in_use, + auto_tier=auto_tier, context=context, context_tier=context_tier, detached_from_spawning_parent_session_id=detached_from_spawning_parent_session_id, @@ -8624,6 +8660,8 @@ def to_dict(self) -> dict: result["version"] = to_int(self.version) if self.already_in_use is not None: result["alreadyInUse"] = from_union([from_none, from_bool], self.already_in_use) + if self.auto_tier is not None: + result["autoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.auto_tier) if self.context is not None: result["context"] = from_union([from_none, lambda x: to_class(WorkingDirectoryContext, x)], self.context) if self.context_tier is not None: @@ -11637,6 +11675,11 @@ class AgentInterruptedCancelPhase(Enum): MID_STREAM = "mid_stream" +class AssistantMessageToolRequestCallerType(Enum): + "Hosted program caller type" + PROGRAM = "program" + + class AssistantMessageToolRequestType(Enum): "Tool call type: \"function\" for standard tool calls, \"custom\" for grammar-based tool calls. Defaults to \"function\" when absent." # Standard function-style tool call. @@ -11695,6 +11738,16 @@ class AutoModeSwitchResponse(Enum): NO = "no" +class AutoTier(Enum): + "Routing preference used when the session model is `auto`." + # Optimize for efficiency. + EFFICIENCY = "efficiency" + # Balance efficiency and intelligence. + BALANCE = "balance" + # Optimize for intelligence. + INTELLIGENCE = "intelligence" + + class AutopilotObjectiveChangedOperation(Enum): "The type of operation performed on the autopilot objective state file" # Autopilot objective state file was created for a new objective. @@ -12487,6 +12540,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AssistantMessageServerTools", "AssistantMessageStartData", "AssistantMessageToolRequest", + "AssistantMessageToolRequestCaller", + "AssistantMessageToolRequestCallerType", "AssistantMessageToolRequestType", "AssistantReasoningData", "AssistantReasoningDeltaData", @@ -12530,6 +12585,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AutoModeSwitchCompletedData", "AutoModeSwitchRequestedData", "AutoModeSwitchResponse", + "AutoTier", "AutopilotObjectiveChangedOperation", "AutopilotObjectiveChangedStatus", "BinaryAssetReference", diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index e22c888f23..6c2f9c8eaf 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -10,10 +10,11 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use super::session_events::{ - AbortReason, ContextTier, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, - McpServerStatus, ModelChangeSource, OmittedBinaryOmittedReason, PermissionMode, - PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionLimitsConfig, SessionMode, - ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity, + AbortReason, AutoTier, ContextTier, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, + McpServerSource, McpServerStatus, ModelChangeSource, OmittedBinaryOmittedReason, + PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionLimitsConfig, + SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, + Verbosity, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -202,6 +203,8 @@ pub mod rpc_methods { pub const SESSION_SEND: &str = "session.send"; /// `session.sendMessages` pub const SESSION_SENDMESSAGES: &str = "session.sendMessages"; + /// `session.sandbox.getEnforcementStatus` + pub const SESSION_SANDBOX_GETENFORCEMENTSTATUS: &str = "session.sandbox.getEnforcementStatus"; /// `session.sendSystemNotification` pub const SESSION_SENDSYSTEMNOTIFICATION: &str = "session.sendSystemNotification"; /// `session.abort` @@ -3019,6 +3022,9 @@ pub struct CanvasProviderUnregisterRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CapiSessionOptions { + /// Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. #[serde(skip_serializing_if = "Option::is_none")] pub enable_web_socket_responses: Option, @@ -4411,6 +4417,9 @@ pub struct DiscoveredMcpServer { pub struct EnqueueCommandParams { /// Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. pub command: String, + /// Optional user-facing text for the queue row. The command string is shown when omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_text: Option, } /// Indicates whether the command was accepted into the local execution queue. @@ -14756,6 +14765,26 @@ pub struct SandboxConfig { pub user_policy: Option, } +/// Managed sandbox enforcement state for a session. +/// +///
+/// +/// **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 SandboxEnforcementStatus { + /// Whether an enforcement failure has permanently blocked the session. + pub blocked: bool, + /// The first sandbox enforcement failure that blocked the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Whether the effective managed policy requires an available sandbox backend. + pub required: bool, +} + /// Register an absolute-time scheduled prompt. /// ///
@@ -21992,6 +22021,41 @@ pub struct SessionSendMessagesResult { pub message_ids: Vec, } +/// Identifies the target session. +/// +///
+/// +/// **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 SessionSandboxGetEnforcementStatusParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Managed sandbox enforcement state for a session. +/// +///
+/// +/// **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 SessionSandboxGetEnforcementStatusResult { + /// Whether an enforcement failure has permanently blocked the session. + pub blocked: bool, + /// The first sandbox enforcement failure that blocked the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Whether the effective managed policy requires an available sandbox backend. + pub required: bool, +} + /// Result of aborting the current turn /// ///
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 4d5b7f1538..99fbd099ae 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -3185,6 +3185,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.sandbox.*` sub-namespace. + pub fn sandbox(&self) -> SessionRpcSandbox<'a> { + SessionRpcSandbox { + session: self.session, + } + } + /// `session.schedule.*` sub-namespace. pub fn schedule(&self) -> SessionRpcSchedule<'a> { SessionRpcSchedule { @@ -9413,6 +9420,42 @@ impl<'a> SessionRpcRemote<'a> { } } +/// `session.sandbox.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcSandbox<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcSandbox<'a> { + /// Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. + /// + /// Wire method: `session.sandbox.getEnforcementStatus`. + /// + /// # Returns + /// + /// Managed sandbox enforcement state for a session. + /// + ///
+ /// + /// **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 get_enforcement_status(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_SANDBOX_GETENFORCEMENTSTATUS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.schedule.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcSchedule<'a> { diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 79284b1671..266be4895a 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -939,6 +939,9 @@ pub struct SessionStartData { /// Whether the session was already in use by another client at start time #[serde(skip_serializing_if = "Option::is_none")] pub already_in_use: Option, + /// Auto routing preference selected at session creation time + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Working directory and git context at session start #[serde(skip_serializing_if = "Option::is_none")] pub context: Option, @@ -988,6 +991,9 @@ pub struct SessionResumeData { /// Whether the session was already in use by another client at resume time #[serde(skip_serializing_if = "Option::is_none")] pub already_in_use: Option, + /// Auto routing preference active at resume time + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Updated working directory and git context at resume time #[serde(skip_serializing_if = "Option::is_none")] pub context: Option, @@ -2527,6 +2533,16 @@ pub struct AssistantMessageServerTools { pub raw_content_blocks: Option>, } +/// Hosted program that requested this client tool call +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssistantMessageToolRequestCaller { + /// Provider-assigned identifier for the hosted caller. + pub caller_id: String, + /// Kind of hosted caller that requested the client tool call. + pub r#type: AssistantMessageToolRequestCallerType, +} + /// A tool invocation request from the assistant #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -2534,6 +2550,9 @@ pub struct AssistantMessageToolRequest { /// Arguments to pass to the tool, format depends on the tool #[serde(skip_serializing_if = "Option::is_none")] pub arguments: Option, + /// Hosted program that requested this client tool call + #[serde(skip_serializing_if = "Option::is_none")] + pub caller: Option, /// Resolved intention summary describing what this specific call does #[serde(skip_serializing_if = "Option::is_none")] pub intention_summary: Option, @@ -6321,6 +6340,24 @@ pub struct McpAppToolCallCompleteData { pub tool_name: String, } +/// Routing preference used when the session model is `auto`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AutoTier { + /// Optimize for efficiency. + #[serde(rename = "efficiency")] + Efficiency, + /// Balance efficiency and intelligence. + #[serde(rename = "balance")] + Balance, + /// Optimize for intelligence. + #[serde(rename = "intelligence")] + Intelligence, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Hosting platform type of the repository (github or ado) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum WorkingDirectoryContextHostType { @@ -6939,6 +6976,17 @@ pub enum CitationProvider { Unknown, } +/// Hosted program caller type +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AssistantMessageToolRequestCallerType { + #[serde(rename = "program")] + Program, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AssistantUsageApiEndpoint { diff --git a/rust/tests/e2e/commands.rs b/rust/tests/e2e/commands.rs index 2e15c24a0f..78c8e07616 100644 --- a/rust/tests/e2e/commands.rs +++ b/rust/tests/e2e/commands.rs @@ -210,6 +210,7 @@ async fn session_commands_enqueue_and_respond_to_queued_command() { .commands() .enqueue(EnqueueCommandParams { command: "/help".to_string(), + display_text: None, }) .await .expect("enqueue command"); diff --git a/rust/tests/e2e/rpc_queue.rs b/rust/tests/e2e/rpc_queue.rs index 6f4f881659..d5f1228302 100644 --- a/rust/tests/e2e/rpc_queue.rs +++ b/rust/tests/e2e/rpc_queue.rs @@ -153,6 +153,7 @@ async fn pendingitems_reports_queued_command_and_remove_and_clear_update_queue() .commands() .enqueue(EnqueueCommandParams { command: first_command, + display_text: None, }) .await .expect("enqueue command"); @@ -167,6 +168,7 @@ async fn pendingitems_reports_queued_command_and_remove_and_clear_update_queue() .commands() .enqueue(EnqueueCommandParams { command: second_command.clone(), + display_text: None, }) .await .expect("enqueue second command"); @@ -187,6 +189,7 @@ async fn pendingitems_reports_queued_command_and_remove_and_clear_update_queue() .commands() .enqueue(EnqueueCommandParams { command: third_command.clone(), + display_text: None, }) .await .expect("enqueue third command"); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index bdb4d095b7..d4be520fe7 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -143,12 +143,22 @@ function placeholderToQuicktypeIdentifiers(placeholder: string): string[] { return [...new Set([basic, basic.replace(/Mcp/g, "MCP")])]; } -function postProcessExternalRefsForPython( +export function postProcessExternalRefsForPython( code: string, placeholderToReal: Map, externalEnumNames: Set = new Set() ): string { for (const [placeholder, realName] of placeholderToReal) { + const markerProperty = `__externalRefMarker_${placeholder}`; + const markerClass = [ + ...code.matchAll( + /(?:^|\n)(@dataclass\r?\nclass (\w+)\b[\s\S]*?)(?=\n@dataclass\b|\nclass\s+\w|\ndef\s+\w|$)/g + ), + ].find((match) => match[1].includes(`"${markerProperty}"`)); + if (markerClass) { + code = code.replace(markerClass[0], "\n"); + code = code.replace(new RegExp(`\\b${escapeRegExp(markerClass[2])}\\b`, "g"), realName); + } for (const quicktypeName of placeholderToQuicktypeIdentifiers(placeholder)) { code = code.replace( new RegExp( diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 710d725318..8e1ab03a06 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.82", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -472,8 +472,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.82-0", - "integrity": "sha512-fSZVNAzFFYaS6btYD0+cKF7SrrtOklhpkPs/cIMZY7Fgxoa6rfZrlTWXlQNgNIdwLp51XxwTfxnZO+D9+MQ5yg==", + "version": "1.0.82", + "integrity": "sha512-+mDIwBO3dCpL3k2rVLc4+1tFzqcVBTJNA/0co+okEpmCgcHjaz91Cqq7++pJKN5yJQuGC6bKangm7PSoheI1Xw==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -483,19 +483,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.82-0", - "@github/copilot-darwin-x64": "1.0.82-0", - "@github/copilot-linux-arm64": "1.0.82-0", - "@github/copilot-linux-x64": "1.0.82-0", - "@github/copilot-linuxmusl-arm64": "1.0.82-0", - "@github/copilot-linuxmusl-x64": "1.0.82-0", - "@github/copilot-win32-arm64": "1.0.82-0", - "@github/copilot-win32-x64": "1.0.82-0" + "@github/copilot-darwin-arm64": "1.0.82", + "@github/copilot-darwin-x64": "1.0.82", + "@github/copilot-linux-arm64": "1.0.82", + "@github/copilot-linux-x64": "1.0.82", + "@github/copilot-linuxmusl-arm64": "1.0.82", + "@github/copilot-linuxmusl-x64": "1.0.82", + "@github/copilot-win32-arm64": "1.0.82", + "@github/copilot-win32-x64": "1.0.82" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-TzBYfyvxcw3z9Mu7U8TsFo/Nq7m5XS6ahT71aPL+gx/YId0kmenI27b9daXsK6LA1D0gsFGwNBDKINddqntt1g==", + "version": "1.0.82", + "integrity": "sha512-UpVSFA0COmlIakAr7/6WJqJlabtKgY8y5en6La3IxGxXsLlbkGoeevSqyfXvqJyHxaGn0YGpOC1oEPL379JfCw==", "cpu": [ "arm64" ], @@ -510,8 +510,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.82-0", - "integrity": "sha512-Lm/U5Q8kN8yEeBTWKTfIxXAgXaT6zqdBzAAO7lA4DWHZ92AEeZZiVTs6jEWsQ2aWB2uMs6zbn0vi6t+/jIqCGw==", + "version": "1.0.82", + "integrity": "sha512-wMKbxK8fpKsbATjn9dE31Pae67H1xu6dmaCuyXpXjXLsNPVjeLFszJZ30MAOwxRWz4dmA8VvEJZmLgJekojo2Q==", "cpu": [ "x64" ], @@ -526,8 +526,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-YERVMC1Q4p6l6KQHL5rVOI52rWbvgp9IwyzUBaVSGrfFuqu5BEvZ9bgHPsxTYi3Npkt5KVOXEyPMU5rAY09qQQ==", + "version": "1.0.82", + "integrity": "sha512-YDQmh0F+GzWORPmNNxQdcjHcXHxcy08dhMhbAhu4cqtMeA2sZWQqhfk9mJhHppMm9U0uR7h0KAB6nsgw/8Bsuw==", "cpu": [ "arm64" ], @@ -542,8 +542,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.82-0", - "integrity": "sha512-z2hxMVjqt4+xDRFTZv3/0K3X+aqcJhd6zPO2JxCpOVTh5CNZFaWk+XIa2iXAPWxFqdKJsQ4muXMl3zInaAOkRw==", + "version": "1.0.82", + "integrity": "sha512-vqmG9665ktgLHAkGKMjp4uVMdHqLxi8uS9zL+g2pMwZUPoM7JxkaSfOoeyYr99caF7J6VIJTWv4LdRNQyG5jrQ==", "cpu": [ "x64" ], @@ -558,8 +558,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-EcUCv2PKhBzCCvpTaS511VYTDWyhudyIRPvBpc9gFNO3hjlgiNDusf4k9vP6+E3/lHenwGYzMsyRHcYIOy4vcQ==", + "version": "1.0.82", + "integrity": "sha512-14dWfa4rBME55bKmF+Z8V+WzZNgPQZKFFBQX+EOiAgLVV/arQCnQ1m7wwa5oXjQeCIgkS/L9J8uM8jNm6cZbOA==", "cpu": [ "arm64" ], @@ -574,8 +574,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.82-0", - "integrity": "sha512-1fKVjUiZ1tdb0/d/re90EpFGXhlIPfjENp2Wo/2Kj592dWO3+IwM2qV/AOdMQ4pacW5iYHII7nibx1/EYq3LGQ==", + "version": "1.0.82", + "integrity": "sha512-Mauqa2TBjtB8W/1KXF/jJ4MbtwnLvoEQNHYoSyoX+s+nIpttmixmYBJDwKV89ZlQRdjOCuxOgraRLQ9hjjXvNQ==", "cpu": [ "x64" ], @@ -590,8 +590,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-H341wuxQHhwe/yLmORHzwC3DGzFZgGzh+TpwfyK2zjeYVbwDZ3Bax8M+9CzIED9jIBdEfAmfGzxKUHFugpXTtQ==", + "version": "1.0.82", + "integrity": "sha512-PaW9s0GTgM+svtDkB583dZRrhLrO4o10y2ozMmQ0Hi5eB11C24UdC5U81nFlvKPED7+GlnBJIFQ+oHaxHrnp3A==", "cpu": [ "arm64" ], @@ -606,8 +606,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.82-0", - "integrity": "sha512-f1ba3gG8NaoYWFHtHaHcLN4It7mclkWdCOXvwFPqPEwqCEIx/+Zh6VHiOeIcNWRS0elRP6QYDCKaTDy1TW27uQ==", + "version": "1.0.82", + "integrity": "sha512-m4iSROOMEPp1yRIQQwbnO0CDfwB4syESyHoeSLLFuynMR4/ApghAdyBrBQQcTKGsUu3R5rOE64RYlVmyKmuA9A==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index cceca0b959..c7e7647ac3 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.82", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", From b79cef213bfc29d4573e21ec7037fa44c0f1d5d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:16:51 +0000 Subject: [PATCH 05/60] Update @github/copilot to 1.0.83-0 (#2449) * Update @github/copilot to 1.0.83-0 - Updated nodejs and test harness dependencies - Re-ran code generators - Formatted generated code * test: separate canvas and extension opt-ins Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Shay Rojansky Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 2 +- dotnet/src/Generated/SessionEvents.cs | 2 +- dotnet/test/E2E/CanvasE2ETests.cs | 1 - go/rpc/zrpc.go | 26 ++-- go/rpc/zsession_events.go | 2 +- java/pom.xml | 2 +- java/scripts/codegen/package-lock.json | 72 +++++----- java/scripts/codegen/package.json | 2 +- .../copilot/generated/HookStartEvent.java | 2 +- .../rpc/SessionUiEphemeralQueryParams.java | 4 +- .../generated/rpc/SessionsOpenCloud.java | 2 +- ...egisterExtensionToolsOnSessionOptions.java | 2 +- ...RegisterExtensionToolsOnSessionParams.java | 2 +- ...RegisterExtensionToolsOnSessionResult.java | 2 +- nodejs/package-lock.json | 54 +++---- nodejs/package.json | 2 +- nodejs/samples/package-lock.json | 2 +- nodejs/src/generated/rpc.ts | 14 +- nodejs/src/generated/session-events.ts | 2 +- python/copilot/generated/rpc.py | 134 ++++++++---------- rust/src/generated/api_types.rs | 14 +- rust/src/generated/rpc.rs | 2 +- rust/src/generated/session_events.rs | 2 +- rust/tests/e2e/canvas.rs | 1 - test/harness/package-lock.json | 54 +++---- test/harness/package.json | 2 +- 26 files changed, 193 insertions(+), 213 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index c97660f369..c7be742ee2 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -29565,7 +29565,7 @@ internal async Task ConnectAsync(bool? enableGitHubTelemetryForwa return await CopilotClient.InvokeRpcAsync(_rpc, "connect", [request], cancellationToken); } - /// Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility. + /// Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime uses its built-in extension launcher. /// The to monitor for cancellation requests. The default is . [Experimental(Diagnostics.Experimental)] public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancellationToken = default) diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 4c7ebc6ffa..6bfe202bff 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -4741,7 +4741,7 @@ public sealed partial class HookStartData [JsonPropertyName("hookType")] public required string HookType { get; set; } - /// Input data passed to the hook. + /// Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("input")] public JsonElement? Input { get; set; } diff --git a/dotnet/test/E2E/CanvasE2ETests.cs b/dotnet/test/E2E/CanvasE2ETests.cs index deb94a9b24..249440d993 100644 --- a/dotnet/test/E2E/CanvasE2ETests.cs +++ b/dotnet/test/E2E/CanvasE2ETests.cs @@ -123,7 +123,6 @@ private Task CreateCanvasSessionAsync(TestCanvasHandler handler) { OnPermissionRequest = PermissionHandler.ApproveAll, RequestCanvasRenderer = true, - RequestExtensions = true, ExtensionInfo = new ExtensionInfo { Source = "dotnet-sdk-tests", Name = "canvas-provider" }, Canvases = [ diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 706f32bf5f..67121c30ed 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -9944,10 +9944,8 @@ type RegisterExtensionLaunchProviderResult struct { // Internal: RegisterExtensionToolsParams is an internal SDK API and is not part of the // public surface. type RegisterExtensionToolsParams struct { - // In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is - // excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, - // extension discovery/launch moves entirely into the runtime — the CLI passes pure config - // (search paths, disabled ids) via SessionOptions instead. + // In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK + // surface. // Internal: Loader is part of the SDK's internal API surface and is not intended for // external use. Loader any `json:"loader"` @@ -9963,8 +9961,7 @@ type RegisterExtensionToolsParams struct { // Internal: RegisterExtensionToolsResult is an internal SDK API and is not part of the // public surface. type RegisterExtensionToolsResult struct { - // In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an - // explicit `extensions.unregister` RPC in the SDK migration. + // In-process unsubscribe function used only by the CLI. // Internal: Unsubscribe is part of the SDK's internal API surface and is not intended for // external use. Unsubscribe any `json:"unsubscribe"` @@ -12144,10 +12141,8 @@ func (SessionsOpenAttach) Kind() SessionOpenParamsKind { // Experimental: SessionsOpenCloud is part of an experimental API and may change or be // removed. type SessionsOpenCloud struct { - // In-process callback invoked when the cloud task is created (before connection). Marked - // internal because a function reference cannot cross the JSON-RPC boundary. Disappears in - // the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from - // 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. + // In-process callback invoked when the cloud task is created, before connection. Internal + // because function references cannot cross the JSON-RPC boundary. // Internal: OnTaskCreated is part of the SDK's internal API surface and is not intended for // external use. OnTaskCreated any `json:"onTaskCreated,omitempty"` @@ -12881,8 +12876,7 @@ type SessionsPruneOldRequest struct { // Experimental: SessionsRegisterExtensionToolsOnSessionOptions is part of an experimental // API and may change or be removed. type SessionsRegisterExtensionToolsOnSessionOptions struct { - // In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: - // replaced by runtime-side enable/disable RPCs in the SDK migration. + // In-process `() => boolean` gating callback used only by the CLI. // Internal: Enabled is part of the SDK's internal API surface and is not intended for // external use. Enabled any `json:"enabled,omitempty"` @@ -14739,14 +14733,12 @@ type UIElicitationStringOneOfFieldOneOf struct { // removed. type UIEphemeralQueryRequest struct { // In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. - // Marked internal: excluded from the public SDK surface. Replaced by an explicit - // cancellation token + cancel RPC in the SDK migration. + // Internal and excluded from the public SDK surface. // Internal: AbortSignal is part of the SDK's internal API surface and is not intended for // external use. AbortSignal any `json:"abortSignal,omitempty"` // In-process streaming callback `(text) => void` invoked with each token as the model emits - // it. Marked internal: excluded from the public SDK surface. In a process-separated SDK - // this is replaced by a streaming RPC that yields chunks and a final answer. + // it. Internal and excluded from the public SDK surface. // Internal: OnChunk is part of the SDK's internal API surface and is not intended for // external use. OnChunk any `json:"onChunk,omitempty"` @@ -20712,7 +20704,7 @@ func (a *ServerRPC) Ping(ctx context.Context, params *PingRequest) (*PingResult, // RegisterExtensionLaunchProvider registers the calling SDK client as the per-entrypoint // extension launch provider. Call before creating any sessions. When omitted, the runtime -// temporarily falls back to its built-in Node launcher for backward compatibility. +// uses its built-in extension launcher. // // RPC method: registerExtensionLaunchProvider. // Experimental: RegisterExtensionLaunchProvider is an experimental API and may change or be diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 07504e8815..acea96f5b9 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -1258,7 +1258,7 @@ type HookStartData struct { HookInvocationID string `json:"hookInvocationId"` // Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") HookType string `json:"hookType"` - // Input data passed to the hook + // Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. Input any `json:"input,omitempty"` // Tool call ID of the parent tool invocation when this event originates from a sub-agent ParentToolCallID *string `json:"parentToolCallId,omitempty"` diff --git a/java/pom.xml b/java/pom.xml index 5b493fcc55..427bc2320c 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -63,7 +63,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.82 + ^1.0.83-0 true diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 71abcd72dc..fd177ca1e8 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.82", + "@github/copilot": "^1.0.83-0", "json-schema": "^0.4.0", "tsx": "^4.23.12" } @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.82", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.82.tgz", - "integrity": "sha512-+mDIwBO3dCpL3k2rVLc4+1tFzqcVBTJNA/0co+okEpmCgcHjaz91Cqq7++pJKN5yJQuGC6bKangm7PSoheI1Xw==", + "version": "1.0.83-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-0.tgz", + "integrity": "sha512-Nv4IsqsveMgghwaBhgvSBZyIyvsqNBZTqnbVnv69+9+Suyq20vJcv6aB74UcJ7VPCMxIGJJUaJkugEtkMNv6wA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.82", - "@github/copilot-darwin-x64": "1.0.82", - "@github/copilot-linux-arm64": "1.0.82", - "@github/copilot-linux-x64": "1.0.82", - "@github/copilot-linuxmusl-arm64": "1.0.82", - "@github/copilot-linuxmusl-x64": "1.0.82", - "@github/copilot-win32-arm64": "1.0.82", - "@github/copilot-win32-x64": "1.0.82" + "@github/copilot-darwin-arm64": "1.0.83-0", + "@github/copilot-darwin-x64": "1.0.83-0", + "@github/copilot-linux-arm64": "1.0.83-0", + "@github/copilot-linux-x64": "1.0.83-0", + "@github/copilot-linuxmusl-arm64": "1.0.83-0", + "@github/copilot-linuxmusl-x64": "1.0.83-0", + "@github/copilot-win32-arm64": "1.0.83-0", + "@github/copilot-win32-x64": "1.0.83-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.82", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.82.tgz", - "integrity": "sha512-UpVSFA0COmlIakAr7/6WJqJlabtKgY8y5en6La3IxGxXsLlbkGoeevSqyfXvqJyHxaGn0YGpOC1oEPL379JfCw==", + "version": "1.0.83-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-0.tgz", + "integrity": "sha512-0KQjKS9vd4QGxLAbFJcvyv/zsC5kivrtDe0UZhHt/43nUGqoS61DFcsM596/kg75vNE6c9J4gmZ5fUPYef+0hw==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.82", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.82.tgz", - "integrity": "sha512-wMKbxK8fpKsbATjn9dE31Pae67H1xu6dmaCuyXpXjXLsNPVjeLFszJZ30MAOwxRWz4dmA8VvEJZmLgJekojo2Q==", + "version": "1.0.83-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-0.tgz", + "integrity": "sha512-fiyW+hy4c8AI7ONxN623f9cmJGRpbqTztc0jSVXc9z9WwzcWi39X0nxUprRM2l2Dq6YQ3guPCqGl/g1T5bQfQg==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.82", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.82.tgz", - "integrity": "sha512-YDQmh0F+GzWORPmNNxQdcjHcXHxcy08dhMhbAhu4cqtMeA2sZWQqhfk9mJhHppMm9U0uR7h0KAB6nsgw/8Bsuw==", + "version": "1.0.83-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-0.tgz", + "integrity": "sha512-RWbRU+KgEmtAdKp1GQVTqfdwg4Ti/OVmgZGkXq4lMYj3wnBBQcayFpSLHg5ShzDSS0RglD4b8Z27NjPrm7bXxA==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.82", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.82.tgz", - "integrity": "sha512-vqmG9665ktgLHAkGKMjp4uVMdHqLxi8uS9zL+g2pMwZUPoM7JxkaSfOoeyYr99caF7J6VIJTWv4LdRNQyG5jrQ==", + "version": "1.0.83-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-0.tgz", + "integrity": "sha512-5COXUNT+jDfkeyqrymZMvhTogkBYUXt+wuRwKrK6ol5vaw5SoDP1DYbI2hIEfoUj4g7XTHLUCD1s3lw8eicqUA==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.82", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.82.tgz", - "integrity": "sha512-14dWfa4rBME55bKmF+Z8V+WzZNgPQZKFFBQX+EOiAgLVV/arQCnQ1m7wwa5oXjQeCIgkS/L9J8uM8jNm6cZbOA==", + "version": "1.0.83-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-0.tgz", + "integrity": "sha512-7sYf364iz6s97ClviBRQusTKz3S3TgoKniyYv8+aRi5f5w6TL8NTPnGX1bXMeU0VZmk5VKQTlxVRO2yA4uFwpg==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.82", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.82.tgz", - "integrity": "sha512-Mauqa2TBjtB8W/1KXF/jJ4MbtwnLvoEQNHYoSyoX+s+nIpttmixmYBJDwKV89ZlQRdjOCuxOgraRLQ9hjjXvNQ==", + "version": "1.0.83-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-0.tgz", + "integrity": "sha512-jze/f6Yd3Y83kxUa88kXUiwHlZmHDwAqudswdHT6f6q+K1ZEELFGEzbB6Ku4i0L8M6wHXO1EF/zaiSFWQaM4Tw==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.82", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.82.tgz", - "integrity": "sha512-PaW9s0GTgM+svtDkB583dZRrhLrO4o10y2ozMmQ0Hi5eB11C24UdC5U81nFlvKPED7+GlnBJIFQ+oHaxHrnp3A==", + "version": "1.0.83-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-0.tgz", + "integrity": "sha512-93jln98UAJpslMQ7n+wAmCpoOWGEV5lXxV/DaEajySvYrCU33D2yj7d9kl8X2CgaUVBas6sNWSWKtqy+rKJxXQ==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.82", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.82.tgz", - "integrity": "sha512-m4iSROOMEPp1yRIQQwbnO0CDfwB4syESyHoeSLLFuynMR4/ApghAdyBrBQQcTKGsUu3R5rOE64RYlVmyKmuA9A==", + "version": "1.0.83-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-0.tgz", + "integrity": "sha512-+4Htk3CixO1qcOtYegjn33/8bSDdx8QXDpgVBak2D4Y5hzBWPO5IuQoICwvjaW5VOIW+I7Q62RK2pupSjxB38Q==", "cpu": [ "x64" ], diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 1b33a27904..8089a79133 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.82", + "@github/copilot": "^1.0.83-0", "json-schema": "^0.4.0", "tsx": "^4.23.12" } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HookStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/HookStartEvent.java index 3b8b41fafb..030e08caf8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/HookStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HookStartEvent.java @@ -38,7 +38,7 @@ public record HookStartEventData( @JsonProperty("hookInvocationId") String hookInvocationId, /** Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") */ @JsonProperty("hookType") String hookType, - /** Input data passed to the hook */ + /** Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. */ @JsonProperty("input") Object input, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiEphemeralQueryParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiEphemeralQueryParams.java index b384238b28..0f02f7258d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiEphemeralQueryParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiEphemeralQueryParams.java @@ -28,9 +28,9 @@ public record SessionUiEphemeralQueryParams( @JsonProperty("sessionId") String sessionId, /** Question to answer from the current conversation context. */ @JsonProperty("question") String question, - /** In-process streaming callback `(text) => void` invoked with each token as the model emits it. Marked internal: excluded from the public SDK surface. In a process-separated SDK this is replaced by a streaming RPC that yields chunks and a final answer. */ + /** In-process streaming callback `(text) => void` invoked with each token as the model emits it. Internal and excluded from the public SDK surface. */ @JsonProperty("onChunk") Object onChunk, - /** In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Marked internal: excluded from the public SDK surface. Replaced by an explicit cancellation token + cancel RPC in the SDK migration. */ + /** In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Internal and excluded from the public SDK surface. */ @JsonProperty("abortSignal") Object abortSignal ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsOpenCloud.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsOpenCloud.java index 8e4a74bd88..b0be6d42dd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsOpenCloud.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsOpenCloud.java @@ -40,7 +40,7 @@ public final class SessionsOpenCloud extends SessionsOpenParams { @JsonProperty("options") private SessionOpenOptions options; - /** In-process callback invoked when the cloud task is created (before connection). Marked internal because a function reference cannot cross the JSON-RPC boundary. Disappears in the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. */ + /** In-process callback invoked when the cloud task is created, before connection. Internal because function references cannot cross the JSON-RPC boundary. */ @JsonProperty("onTaskCreated") private Object onTaskCreated; diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionOptions.java index 440c01b2a8..e19737e32e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionOptions.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SessionsRegisterExtensionToolsOnSessionOptions( - /** In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: replaced by runtime-side enable/disable RPCs in the SDK migration. */ + /** In-process `() => boolean` gating callback used only by the CLI. */ @JsonProperty("enabled") Object enabled ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionParams.java index d7eb48f2b3..7fcb3322e9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionParams.java @@ -26,7 +26,7 @@ public record SessionsRegisterExtensionToolsOnSessionParams( /** Session to register extension tools on. */ @JsonProperty("sessionId") String sessionId, - /** In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, extension discovery/launch moves entirely into the runtime — the CLI passes pure config (search paths, disabled ids) via SessionOptions instead. */ + /** In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK surface. */ @JsonProperty("loader") Object loader, /** Optional registration options. */ @JsonProperty("options") SessionsRegisterExtensionToolsOnSessionOptions options diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionResult.java index 63cc5fb0fb..5cb5af063b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionResult.java @@ -24,7 +24,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SessionsRegisterExtensionToolsOnSessionResult( - /** In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an explicit `extensions.unregister` RPC in the SDK migration. */ + /** In-process unsubscribe function used only by the CLI. */ @JsonProperty("unsubscribe") Object unsubscribe ) { } diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 6cefab6031..6480e85e29 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.82", + "@github/copilot": "^1.0.83-0", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -658,8 +658,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.82", - "integrity": "sha512-+mDIwBO3dCpL3k2rVLc4+1tFzqcVBTJNA/0co+okEpmCgcHjaz91Cqq7++pJKN5yJQuGC6bKangm7PSoheI1Xw==", + "version": "1.0.83-0", + "integrity": "sha512-Nv4IsqsveMgghwaBhgvSBZyIyvsqNBZTqnbVnv69+9+Suyq20vJcv6aB74UcJ7VPCMxIGJJUaJkugEtkMNv6wA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -668,19 +668,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.82", - "@github/copilot-darwin-x64": "1.0.82", - "@github/copilot-linux-arm64": "1.0.82", - "@github/copilot-linux-x64": "1.0.82", - "@github/copilot-linuxmusl-arm64": "1.0.82", - "@github/copilot-linuxmusl-x64": "1.0.82", - "@github/copilot-win32-arm64": "1.0.82", - "@github/copilot-win32-x64": "1.0.82" + "@github/copilot-darwin-arm64": "1.0.83-0", + "@github/copilot-darwin-x64": "1.0.83-0", + "@github/copilot-linux-arm64": "1.0.83-0", + "@github/copilot-linux-x64": "1.0.83-0", + "@github/copilot-linuxmusl-arm64": "1.0.83-0", + "@github/copilot-linuxmusl-x64": "1.0.83-0", + "@github/copilot-win32-arm64": "1.0.83-0", + "@github/copilot-win32-x64": "1.0.83-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.82", - "integrity": "sha512-UpVSFA0COmlIakAr7/6WJqJlabtKgY8y5en6La3IxGxXsLlbkGoeevSqyfXvqJyHxaGn0YGpOC1oEPL379JfCw==", + "version": "1.0.83-0", + "integrity": "sha512-0KQjKS9vd4QGxLAbFJcvyv/zsC5kivrtDe0UZhHt/43nUGqoS61DFcsM596/kg75vNE6c9J4gmZ5fUPYef+0hw==", "cpu": [ "arm64" ], @@ -694,8 +694,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.82", - "integrity": "sha512-wMKbxK8fpKsbATjn9dE31Pae67H1xu6dmaCuyXpXjXLsNPVjeLFszJZ30MAOwxRWz4dmA8VvEJZmLgJekojo2Q==", + "version": "1.0.83-0", + "integrity": "sha512-fiyW+hy4c8AI7ONxN623f9cmJGRpbqTztc0jSVXc9z9WwzcWi39X0nxUprRM2l2Dq6YQ3guPCqGl/g1T5bQfQg==", "cpu": [ "x64" ], @@ -709,8 +709,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.82", - "integrity": "sha512-YDQmh0F+GzWORPmNNxQdcjHcXHxcy08dhMhbAhu4cqtMeA2sZWQqhfk9mJhHppMm9U0uR7h0KAB6nsgw/8Bsuw==", + "version": "1.0.83-0", + "integrity": "sha512-RWbRU+KgEmtAdKp1GQVTqfdwg4Ti/OVmgZGkXq4lMYj3wnBBQcayFpSLHg5ShzDSS0RglD4b8Z27NjPrm7bXxA==", "cpu": [ "arm64" ], @@ -724,8 +724,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.82", - "integrity": "sha512-vqmG9665ktgLHAkGKMjp4uVMdHqLxi8uS9zL+g2pMwZUPoM7JxkaSfOoeyYr99caF7J6VIJTWv4LdRNQyG5jrQ==", + "version": "1.0.83-0", + "integrity": "sha512-5COXUNT+jDfkeyqrymZMvhTogkBYUXt+wuRwKrK6ol5vaw5SoDP1DYbI2hIEfoUj4g7XTHLUCD1s3lw8eicqUA==", "cpu": [ "x64" ], @@ -739,8 +739,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.82", - "integrity": "sha512-14dWfa4rBME55bKmF+Z8V+WzZNgPQZKFFBQX+EOiAgLVV/arQCnQ1m7wwa5oXjQeCIgkS/L9J8uM8jNm6cZbOA==", + "version": "1.0.83-0", + "integrity": "sha512-7sYf364iz6s97ClviBRQusTKz3S3TgoKniyYv8+aRi5f5w6TL8NTPnGX1bXMeU0VZmk5VKQTlxVRO2yA4uFwpg==", "cpu": [ "arm64" ], @@ -754,8 +754,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.82", - "integrity": "sha512-Mauqa2TBjtB8W/1KXF/jJ4MbtwnLvoEQNHYoSyoX+s+nIpttmixmYBJDwKV89ZlQRdjOCuxOgraRLQ9hjjXvNQ==", + "version": "1.0.83-0", + "integrity": "sha512-jze/f6Yd3Y83kxUa88kXUiwHlZmHDwAqudswdHT6f6q+K1ZEELFGEzbB6Ku4i0L8M6wHXO1EF/zaiSFWQaM4Tw==", "cpu": [ "x64" ], @@ -769,8 +769,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.82", - "integrity": "sha512-PaW9s0GTgM+svtDkB583dZRrhLrO4o10y2ozMmQ0Hi5eB11C24UdC5U81nFlvKPED7+GlnBJIFQ+oHaxHrnp3A==", + "version": "1.0.83-0", + "integrity": "sha512-93jln98UAJpslMQ7n+wAmCpoOWGEV5lXxV/DaEajySvYrCU33D2yj7d9kl8X2CgaUVBas6sNWSWKtqy+rKJxXQ==", "cpu": [ "arm64" ], @@ -784,8 +784,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.82", - "integrity": "sha512-m4iSROOMEPp1yRIQQwbnO0CDfwB4syESyHoeSLLFuynMR4/ApghAdyBrBQQcTKGsUu3R5rOE64RYlVmyKmuA9A==", + "version": "1.0.83-0", + "integrity": "sha512-+4Htk3CixO1qcOtYegjn33/8bSDdx8QXDpgVBak2D4Y5hzBWPO5IuQoICwvjaW5VOIW+I7Q62RK2pupSjxB38Q==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index ff54fb3682..e1744c4fdc 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.82", + "@github/copilot": "^1.0.83-0", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 16a17c7d21..0ef1a077b2 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.82", + "@github/copilot": "^1.0.83-0", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 024dd3a5bb..32a002dd01 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -16047,7 +16047,7 @@ export interface RegisterExtensionToolsParams { */ sessionId: string; /** - * In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, extension discovery/launch moves entirely into the runtime — the CLI passes pure config (search paths, disabled ids) via SessionOptions instead. + * In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK surface. * * @internal * @@ -16065,7 +16065,7 @@ export interface RegisterExtensionToolsParams { /** @experimental */ export interface SessionsRegisterExtensionToolsOnSessionOptions { /** - * In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: replaced by runtime-side enable/disable RPCs in the SDK migration. + * In-process `() => boolean` gating callback used only by the CLI. * * @internal */ @@ -16081,7 +16081,7 @@ export interface SessionsRegisterExtensionToolsOnSessionOptions { /** @internal */ export interface RegisterExtensionToolsResult { /** - * In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an explicit `extensions.unregister` RPC in the SDK migration. + * In-process unsubscribe function used only by the CLI. * * @internal * @@ -18660,7 +18660,7 @@ export interface SessionsOpenCloud { owner?: string; options?: SessionOpenOptions; /** - * In-process callback invoked when the cloud task is created (before connection). Marked internal because a function reference cannot cross the JSON-RPC boundary. Disappears in the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. + * In-process callback invoked when the cloud task is created, before connection. Internal because function references cannot cross the JSON-RPC boundary. * * @internal */ @@ -21644,13 +21644,13 @@ export interface UIEphemeralQueryRequest { */ question: string; /** - * In-process streaming callback `(text) => void` invoked with each token as the model emits it. Marked internal: excluded from the public SDK surface. In a process-separated SDK this is replaced by a streaming RPC that yields chunks and a final answer. + * In-process streaming callback `(text) => void` invoked with each token as the model emits it. Internal and excluded from the public SDK surface. * * @internal */ onChunk?: OpaqueInProcessValue; /** - * In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Marked internal: excluded from the public SDK surface. Replaced by an explicit cancellation token + cancel RPC in the SDK migration. + * In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Internal and excluded from the public SDK surface. * * @internal */ @@ -23001,7 +23001,7 @@ export function createServerRpc(connection: MessageConnection) { connection.sendRequest("extensions.disable", params), }, /** - * Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility. + * Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime uses its built-in extension launcher. * * @experimental */ diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index cb845443aa..c7a610abef 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -7019,7 +7019,7 @@ export interface HookStartData { */ hookType: string; /** - * Input data passed to the hook + * Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. */ input?: JsonValue; /** diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 84f3718b16..1340d726be 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -9175,9 +9175,7 @@ class SessionsRegisterExtensionToolsOnSessionOptions: # Internal: this field is an internal SDK API and is not part of the public surface. enabled: Any = None - """In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: - replaced by runtime-side enable/disable RPCs in the SDK migration. - """ + """In-process `() => boolean` gating callback used only by the CLI.""" @staticmethod def from_dict(obj: Any) -> 'SessionsRegisterExtensionToolsOnSessionOptions': @@ -9191,6 +9189,26 @@ def to_dict(self) -> dict: result["enabled"] = self.enabled return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _RegisterExtensionToolsResult: + """Handle for releasing the extension tool registration.""" + + unsubscribe: Any + """In-process unsubscribe function used only by the CLI.""" + + @staticmethod + def from_dict(obj: Any) -> '_RegisterExtensionToolsResult': + assert isinstance(obj, dict) + unsubscribe = obj.get("unsubscribe") + return _RegisterExtensionToolsResult(unsubscribe) + + def to_dict(self) -> dict: + result: dict = {} + result["unsubscribe"] = self.unsubscribe + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ReleaseEventInterestParams: @@ -13718,6 +13736,42 @@ class UIElicitationSchemaPropertyNumberType(Enum): INTEGER = "integer" NUMBER = "number" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class UIEphemeralQueryRequest: + """Transient question to answer without adding it to conversation history.""" + + question: str + """Question to answer from the current conversation context.""" + + # Internal: this field is an internal SDK API and is not part of the public surface. + abort_signal: Any = None + """In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. + Internal and excluded from the public SDK surface. + """ + # Internal: this field is an internal SDK API and is not part of the public surface. + on_chunk: Any = None + """In-process streaming callback `(text) => void` invoked with each token as the model emits + it. Internal and excluded from the public SDK surface. + """ + + @staticmethod + def from_dict(obj: Any) -> 'UIEphemeralQueryRequest': + assert isinstance(obj, dict) + question = from_str(obj.get("question")) + abort_signal = obj.get("abortSignal") + on_chunk = obj.get("onChunk") + return UIEphemeralQueryRequest(question, abort_signal, on_chunk) + + def to_dict(self) -> dict: + result: dict = {} + result["question"] = from_str(self.question) + if self.abort_signal is not None: + result["abortSignal"] = self.abort_signal + if self.on_chunk is not None: + result["onChunk"] = self.on_chunk + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class UIEphemeralQueryResult: @@ -22323,10 +22377,8 @@ class _RegisterExtensionToolsParams: """Params to attach an extension loader's tools to a session.""" loader: Any - """In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is - excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, - extension discovery/launch moves entirely into the runtime — the CLI passes pure config - (search paths, disabled ids) via SessionOptions instead. + """In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK + surface. """ session_id: str """Session to register extension tools on.""" @@ -34223,28 +34275,6 @@ def to_dict(self) -> dict: result["title"] = from_union([from_str, from_none], self.title) return result -# Experimental: this type is part of an experimental API and may change or be removed. -# Internal: this type is an internal SDK API and is not part of the public surface. -@dataclass -class _RegisterExtensionToolsResult: - """Handle for releasing the extension tool registration.""" - - unsubscribe: Any - """In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an - explicit `extensions.unregister` RPC in the SDK migration. - """ - - @staticmethod - def from_dict(obj: Any) -> '_RegisterExtensionToolsResult': - assert isinstance(obj, dict) - unsubscribe = obj.get("unsubscribe") - return _RegisterExtensionToolsResult(unsubscribe) - - def to_dict(self) -> dict: - result: dict = {} - result["unsubscribe"] = self.unsubscribe - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionLimitPredictionDetails: @@ -34505,10 +34535,8 @@ class SessionsOpenCloud: # Internal: this field is an internal SDK API and is not part of the public surface. on_task_created: Any = None - """In-process callback invoked when the cloud task is created (before connection). Marked - internal because a function reference cannot cross the JSON-RPC boundary. Disappears in - the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from - 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. + """In-process callback invoked when the cloud task is created, before connection. Internal + because function references cannot cross the JSON-RPC boundary. """ options: SessionOpenOptions | None = None """Session options for cloud session creation.""" @@ -34970,44 +34998,6 @@ def to_dict(self) -> dict: result["tools"] = from_list(lambda x: to_class(ProtocolExternalToolDefinition, x), self.tools) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class UIEphemeralQueryRequest: - """Transient question to answer without adding it to conversation history.""" - - question: str - """Question to answer from the current conversation context.""" - - # Internal: this field is an internal SDK API and is not part of the public surface. - abort_signal: Any = None - """In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. - Marked internal: excluded from the public SDK surface. Replaced by an explicit - cancellation token + cancel RPC in the SDK migration. - """ - # Internal: this field is an internal SDK API and is not part of the public surface. - on_chunk: Any = None - """In-process streaming callback `(text) => void` invoked with each token as the model emits - it. Marked internal: excluded from the public SDK surface. In a process-separated SDK - this is replaced by a streaming RPC that yields chunks and a final answer. - """ - - @staticmethod - def from_dict(obj: Any) -> 'UIEphemeralQueryRequest': - assert isinstance(obj, dict) - question = from_str(obj.get("question")) - abort_signal = obj.get("abortSignal") - on_chunk = obj.get("onChunk") - return UIEphemeralQueryRequest(question, abort_signal, on_chunk) - - def to_dict(self) -> dict: - result: dict = {} - result["question"] = from_str(self.question) - if self.abort_signal is not None: - result["abortSignal"] = self.abort_signal - if self.on_chunk is not None: - result["onChunk"] = self.on_chunk - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class UpdateSubagentSettingsRequest: @@ -39625,7 +39615,7 @@ async def ping(self, params: PingRequest, *, timeout: float | None = None) -> Pi return PingResult.from_dict(await self._client.request("ping", params_dict, **_timeout_kwargs(timeout))) async def register_extension_launch_provider(self, *, timeout: float | None = None) -> None: - "Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility.\n\n.. warning:: This API is experimental and may change or be removed in future versions." + "Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime uses its built-in extension launcher.\n\n.. warning:: This API is experimental and may change or be removed in future versions." await self._client.request("registerExtensionLaunchProvider", {}, **_timeout_kwargs(timeout)) diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 6c2f9c8eaf..701ae61403 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -14173,7 +14173,7 @@ pub struct RegisterEventInterestResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionsRegisterExtensionToolsOnSessionOptions { - /// In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: replaced by runtime-side enable/disable RPCs in the SDK migration. + /// In-process `() => boolean` gating callback used only by the CLI. #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) enabled: Option, @@ -14190,7 +14190,7 @@ pub struct SessionsRegisterExtensionToolsOnSessionOptions { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct RegisterExtensionToolsParams { - /// In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, extension discovery/launch moves entirely into the runtime — the CLI passes pure config (search paths, disabled ids) via SessionOptions instead. + /// In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK surface. #[doc(hidden)] pub(crate) loader: serde_json::Value, /// Optional registration options. @@ -14211,7 +14211,7 @@ pub(crate) struct RegisterExtensionToolsParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct RegisterExtensionToolsResult { - /// In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an explicit `extensions.unregister` RPC in the SDK migration. + /// In-process unsubscribe function used only by the CLI. #[doc(hidden)] pub(crate) unsubscribe: serde_json::Value, } @@ -17062,7 +17062,7 @@ pub struct SessionsOpenRemote { pub struct SessionsOpenCloud { /// Create a new cloud (coding-agent) session. pub kind: SessionsOpenCloudKind, - /// In-process callback invoked when the cloud task is created (before connection). Marked internal because a function reference cannot cross the JSON-RPC boundary. Disappears in the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. + /// In-process callback invoked when the cloud task is created, before connection. Internal because function references cannot cross the JSON-RPC boundary. #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) on_task_created: Option, @@ -20231,11 +20231,11 @@ pub struct UIElicitationStringOneOfField { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UIEphemeralQueryRequest { - /// In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Marked internal: excluded from the public SDK surface. Replaced by an explicit cancellation token + cancel RPC in the SDK migration. + /// In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Internal and excluded from the public SDK surface. #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) abort_signal: Option, - /// In-process streaming callback `(text) => void` invoked with each token as the model emits it. Marked internal: excluded from the public SDK surface. In a process-separated SDK this is replaced by a streaming RPC that yields chunks and a final answer. + /// In-process streaming callback `(text) => void` invoked with each token as the model emits it. Internal and excluded from the public SDK surface. #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) on_chunk: Option, @@ -21971,7 +21971,7 @@ pub struct SessionsGetRemoteControlStatusResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct SessionsRegisterExtensionToolsOnSessionResult { - /// In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an explicit `extensions.unregister` RPC in the SDK migration. + /// In-process unsubscribe function used only by the CLI. #[doc(hidden)] pub(crate) unsubscribe: serde_json::Value, } diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 99fbd099ae..6f5895d856 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -211,7 +211,7 @@ impl<'a> ClientRpc<'a> { Ok(serde_json::from_value(_value)?) } - /// Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility. + /// Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime uses its built-in extension launcher. /// /// Wire method: `registerExtensionLaunchProvider`. /// diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 266be4895a..4b04b03444 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -4042,7 +4042,7 @@ pub struct HookStartData { pub hook_invocation_id: String, /// Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") pub hook_type: String, - /// Input data passed to the hook + /// Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. #[serde(skip_serializing_if = "Option::is_none")] pub input: Option, /// Tool call ID of the parent tool invocation when this event originates from a sub-agent diff --git a/rust/tests/e2e/canvas.rs b/rust/tests/e2e/canvas.rs index 2418e9e5a6..a2873eed23 100644 --- a/rust/tests/e2e/canvas.rs +++ b/rust/tests/e2e/canvas.rs @@ -64,7 +64,6 @@ fn canvas_session_config( ctx.approve_all_session_config() .with_request_canvas_renderer(true) - .with_request_extensions(true) .with_extension_info(ExtensionInfo::new("rust-sdk-tests", "canvas-provider")) .with_canvases([decl]) .with_canvas_handler(handler) diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 8e1ab03a06..a83d73d0a9 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.82", + "@github/copilot": "^1.0.83-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -472,8 +472,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.82", - "integrity": "sha512-+mDIwBO3dCpL3k2rVLc4+1tFzqcVBTJNA/0co+okEpmCgcHjaz91Cqq7++pJKN5yJQuGC6bKangm7PSoheI1Xw==", + "version": "1.0.83-0", + "integrity": "sha512-Nv4IsqsveMgghwaBhgvSBZyIyvsqNBZTqnbVnv69+9+Suyq20vJcv6aB74UcJ7VPCMxIGJJUaJkugEtkMNv6wA==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -483,19 +483,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.82", - "@github/copilot-darwin-x64": "1.0.82", - "@github/copilot-linux-arm64": "1.0.82", - "@github/copilot-linux-x64": "1.0.82", - "@github/copilot-linuxmusl-arm64": "1.0.82", - "@github/copilot-linuxmusl-x64": "1.0.82", - "@github/copilot-win32-arm64": "1.0.82", - "@github/copilot-win32-x64": "1.0.82" + "@github/copilot-darwin-arm64": "1.0.83-0", + "@github/copilot-darwin-x64": "1.0.83-0", + "@github/copilot-linux-arm64": "1.0.83-0", + "@github/copilot-linux-x64": "1.0.83-0", + "@github/copilot-linuxmusl-arm64": "1.0.83-0", + "@github/copilot-linuxmusl-x64": "1.0.83-0", + "@github/copilot-win32-arm64": "1.0.83-0", + "@github/copilot-win32-x64": "1.0.83-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.82", - "integrity": "sha512-UpVSFA0COmlIakAr7/6WJqJlabtKgY8y5en6La3IxGxXsLlbkGoeevSqyfXvqJyHxaGn0YGpOC1oEPL379JfCw==", + "version": "1.0.83-0", + "integrity": "sha512-0KQjKS9vd4QGxLAbFJcvyv/zsC5kivrtDe0UZhHt/43nUGqoS61DFcsM596/kg75vNE6c9J4gmZ5fUPYef+0hw==", "cpu": [ "arm64" ], @@ -510,8 +510,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.82", - "integrity": "sha512-wMKbxK8fpKsbATjn9dE31Pae67H1xu6dmaCuyXpXjXLsNPVjeLFszJZ30MAOwxRWz4dmA8VvEJZmLgJekojo2Q==", + "version": "1.0.83-0", + "integrity": "sha512-fiyW+hy4c8AI7ONxN623f9cmJGRpbqTztc0jSVXc9z9WwzcWi39X0nxUprRM2l2Dq6YQ3guPCqGl/g1T5bQfQg==", "cpu": [ "x64" ], @@ -526,8 +526,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.82", - "integrity": "sha512-YDQmh0F+GzWORPmNNxQdcjHcXHxcy08dhMhbAhu4cqtMeA2sZWQqhfk9mJhHppMm9U0uR7h0KAB6nsgw/8Bsuw==", + "version": "1.0.83-0", + "integrity": "sha512-RWbRU+KgEmtAdKp1GQVTqfdwg4Ti/OVmgZGkXq4lMYj3wnBBQcayFpSLHg5ShzDSS0RglD4b8Z27NjPrm7bXxA==", "cpu": [ "arm64" ], @@ -542,8 +542,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.82", - "integrity": "sha512-vqmG9665ktgLHAkGKMjp4uVMdHqLxi8uS9zL+g2pMwZUPoM7JxkaSfOoeyYr99caF7J6VIJTWv4LdRNQyG5jrQ==", + "version": "1.0.83-0", + "integrity": "sha512-5COXUNT+jDfkeyqrymZMvhTogkBYUXt+wuRwKrK6ol5vaw5SoDP1DYbI2hIEfoUj4g7XTHLUCD1s3lw8eicqUA==", "cpu": [ "x64" ], @@ -558,8 +558,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.82", - "integrity": "sha512-14dWfa4rBME55bKmF+Z8V+WzZNgPQZKFFBQX+EOiAgLVV/arQCnQ1m7wwa5oXjQeCIgkS/L9J8uM8jNm6cZbOA==", + "version": "1.0.83-0", + "integrity": "sha512-7sYf364iz6s97ClviBRQusTKz3S3TgoKniyYv8+aRi5f5w6TL8NTPnGX1bXMeU0VZmk5VKQTlxVRO2yA4uFwpg==", "cpu": [ "arm64" ], @@ -574,8 +574,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.82", - "integrity": "sha512-Mauqa2TBjtB8W/1KXF/jJ4MbtwnLvoEQNHYoSyoX+s+nIpttmixmYBJDwKV89ZlQRdjOCuxOgraRLQ9hjjXvNQ==", + "version": "1.0.83-0", + "integrity": "sha512-jze/f6Yd3Y83kxUa88kXUiwHlZmHDwAqudswdHT6f6q+K1ZEELFGEzbB6Ku4i0L8M6wHXO1EF/zaiSFWQaM4Tw==", "cpu": [ "x64" ], @@ -590,8 +590,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.82", - "integrity": "sha512-PaW9s0GTgM+svtDkB583dZRrhLrO4o10y2ozMmQ0Hi5eB11C24UdC5U81nFlvKPED7+GlnBJIFQ+oHaxHrnp3A==", + "version": "1.0.83-0", + "integrity": "sha512-93jln98UAJpslMQ7n+wAmCpoOWGEV5lXxV/DaEajySvYrCU33D2yj7d9kl8X2CgaUVBas6sNWSWKtqy+rKJxXQ==", "cpu": [ "arm64" ], @@ -606,8 +606,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.82", - "integrity": "sha512-m4iSROOMEPp1yRIQQwbnO0CDfwB4syESyHoeSLLFuynMR4/ApghAdyBrBQQcTKGsUu3R5rOE64RYlVmyKmuA9A==", + "version": "1.0.83-0", + "integrity": "sha512-+4Htk3CixO1qcOtYegjn33/8bSDdx8QXDpgVBak2D4Y5hzBWPO5IuQoICwvjaW5VOIW+I7Q62RK2pupSjxB38Q==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index c7e7647ac3..c2081214ab 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.82", + "@github/copilot": "^1.0.83-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", From 01b27f16c52db654918d6d831c9ae5d24b47bf14 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Mon, 31 Aug 2026 16:50:08 +0000 Subject: [PATCH 06/60] [SDK/Factories] Add Factory Pagination And Completion Options (#2431) * [SDK/Factories] Add Factory Pagination And Completion Options Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Add factory convenience E2E coverage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Clarify factory completion notification guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Isolate factory E2E completion notifications Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 6 + nodejs/docs/factories.md | 29 +++- nodejs/src/extension.ts | 2 + nodejs/src/factory.ts | 38 ++++++ nodejs/src/index.ts | 2 + nodejs/src/session.ts | 12 +- nodejs/test/e2e/factory.e2e.test.ts | 128 ++++++++++++++++-- .../test/e2e/fixtures/factory-extension.mjs | 17 +++ nodejs/test/factory.test.ts | 54 +++++++- 9 files changed, 269 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d695ed6fb6..345308849c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu ## [Unreleased] +### Feature: Node Agent Factories pagination and run notifications + +The experimental Node.js Agent Factories convenience API now supports paginated run history. Existing `session.factory.listRuns()` calls still return the runs array, while calls with `afterSeq`, `beforeSeq`, or `limit` return the full page with cursor and truncation metadata. + +Factory `run` and `resume` options now accept `notifyOnComplete` and `logPhaseNames`. The SDK forwards these options to the Copilot CLI for new and resumed runs. + ### Feature: rotating session-scoped GitHub credentials All six SDKs can now acquire short-lived GitHub credentials through a session-scoped callback. The SDK registers the callback before session create or resume, maps `initial` and `refresh` requests to the owning session, and removes registrations on rollback, replacement, session close, and client close. Static per-session `gitHubToken` credentials remain supported and are mutually exclusive with the callback. diff --git a/nodejs/docs/factories.md b/nodejs/docs/factories.md index 903a3de8b1..23b9d0fed3 100644 --- a/nodejs/docs/factories.md +++ b/nodejs/docs/factories.md @@ -139,6 +139,8 @@ Run by registered name or handle: const run = await session.factory.run("review-changed", { args: { files: ["src/a.ts"] }, limits: { maxAiCredits: 3 }, + notifyOnComplete: true, + logPhaseNames: true, }); if (run.status === "completed") { @@ -153,7 +155,12 @@ The name overload is: ```ts session.factory.run( name: string, - options?: { args?: JsonValue; limits?: FactoryLimits }, + options?: { + args?: JsonValue; + limits?: FactoryLimits; + notifyOnComplete?: boolean; + logPhaseNames?: boolean; + }, ): Promise; ``` @@ -162,6 +169,8 @@ Resume by run ID without resending the name or arguments: ```ts const run = await session.factory.resume(runId, { limits: { maxAiCredits: 6 }, + notifyOnComplete: true, + logPhaseNames: true, }); ``` @@ -170,10 +179,16 @@ The signature is: ```ts session.factory.resume( runId: string, - options?: { limits?: FactoryLimits }, + options?: { + limits?: FactoryLimits; + notifyOnComplete?: boolean; + logPhaseNames?: boolean; + }, ): Promise; ``` +Set `notifyOnComplete` to `true` for factories that are likely to be invoked by an agent, so the originating session is notified when the factory completes. Set it to `false` for factories intended to be invoked programmatically, where the caller awaits the result directly. Set `logPhaseNames` to emit factory phase names to the session transcript. Both options apply to new and resumed runs. + Both resolve with the run envelope (`FactoryRunResult`) for **every** outcome — `completed`, `error`, `halted`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. The model's `run_factory` tool requests permission before the durable row exists; declining it creates no run row. An SDK-initiated run is refused only when the session already has its maximum number of active top-level runs. Pre-execution resume failures throw `FactoryResumeError`, whose `code` is one of `not_found`, `non_resumable`, `already_active`, `factory_already_running`, `factory_limits_invalid`, `factory_session_disposed`, `factory_storage_unavailable`, or `factory_storage_corrupt`. An agent that no longer has a prior run's ID in context can recover it with `factories_manage` and `operation: "runs"`, which lists the session's factory runs with their IDs and statuses. This matters for resume: a run that reached a limit keeps its journal, so resuming it replays completed work for free, while restarting it from scratch pays for that work twice. @@ -219,8 +234,13 @@ The calling session can inspect its own factory runs: ```ts const runs = await session.factory.listRuns(); +const runsPage = await session.factory.listRuns({ + afterSeq, + beforeSeq, + limit, +}); const detail = await session.factory.getRunDetail(runId); -const page = await session.factory.getRunProgress(runId, { +const progressPage = await session.factory.getRunProgress(runId, { phaseId, afterSeq, beforeSeq, @@ -228,7 +248,8 @@ const page = await session.factory.getRunProgress(runId, { }); ``` -- `listRuns()` returns the newest default page of this session's durable factory runs. +- `listRuns()` returns only the runs array from the newest default page of this session's durable factory runs. This overload preserves the original convenience API. +- `listRuns({ afterSeq, beforeSeq, limit })` returns the full page. Its `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` fields let callers continue paging without raw RPC calls. - `getRunDetail(runId)` returns phases, prompt-safe agent summaries, and the latest progress page. - `getRunProgress(runId, options?)` pages progress forward, backward, by phase, or from the latest tail. diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts index d756308734..ac0ccdb7ce 100644 --- a/nodejs/src/extension.ts +++ b/nodejs/src/extension.ts @@ -85,6 +85,8 @@ export { type FactoryRunResult, type FactoryRunStatus, type FactoryRunSummary, + type FactoryListRunsOptions, + type FactoryRunsPage, type FactoryRunDetail, type FactoryProgressPage, type FactoryProgressLine, diff --git a/nodejs/src/factory.ts b/nodejs/src/factory.ts index 8a6c787471..6212f462b4 100644 --- a/nodejs/src/factory.ts +++ b/nodejs/src/factory.ts @@ -4,6 +4,8 @@ import type { FactoryGetRunProgressRequest, + FactoryListRunsRequest, + FactoryListRunsResult, FactoryProgressPage, FactoryRunDetail, FactoryRunResult, @@ -26,6 +28,22 @@ export type { FactoryRunSummary, } from "./generated/rpc.js"; +/** + * Options for paging durable factory runs. + * + * @experimental Part of the experimental Agent Factories surface and may + * change or be removed in future SDK or CLI releases. + */ +export type FactoryListRunsOptions = FactoryListRunsRequest; + +/** + * A page of durable factory runs and its paging metadata. + * + * @experimental Part of the experimental Agent Factories surface and may + * change or be removed in future SDK or CLI releases. + */ +export type FactoryRunsPage = FactoryListRunsResult; + /** * Run statuses a factory run can no longer move away from. * @@ -242,6 +260,10 @@ export interface RunOptions { args?: TArgs; /** Optional per-invocation resource ceiling overrides. */ limits?: FactoryLimits; + /** Whether to notify the originating session when the factory completes. */ + notifyOnComplete?: boolean; + /** Whether to emit factory phase names to the session transcript. */ + logPhaseNames?: boolean; /** * Prior run whose persisted identity, arguments, journal, and accounting should be resumed. * @@ -259,6 +281,10 @@ export interface RunOptions { export interface ResumeOptions { /** Optional per-invocation resource ceiling overrides. */ limits?: FactoryLimits; + /** Whether to notify the originating session when the factory completes. */ + notifyOnComplete?: boolean; + /** Whether to emit factory phase names to the session transcript. */ + logPhaseNames?: boolean; } /** @@ -328,8 +354,20 @@ export interface SessionFactoryApi { waitForRun(runId: string, options?: { signal?: AbortSignal }): Promise; /** * List the newest default page of this session's durable factory runs. + * + * This backwards-compatible overload returns only the runs array. Pass + * paging options to receive the full page, including its cursors and + * truncation metadata. */ listRuns(): Promise; + /** + * Page this session's durable factory runs. + * + * `afterSeq` and `beforeSeq` are exclusive cursors. The result includes + * `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` so callers + * can continue paging without using the raw RPC client. + */ + listRuns(options: FactoryListRunsOptions): Promise; /** Read durable phases, direct agents, and the latest progress tail for a run. */ getRunDetail(runId: string): Promise; /** Page durable progress forward, backward, or from the latest tail. */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 9d55ab1d10..1f4359ff8c 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -209,6 +209,8 @@ export type { FactoryRunResult, FactoryRunStatus, FactoryRunSummary, + FactoryListRunsOptions, + FactoryRunsPage, FactoryRunDetail, FactoryProgressPage, FactoryProgressLine, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 65ff00921c..eb4c6b7561 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -71,6 +71,7 @@ import { FactoryResumeError, isFactoryRunTerminal, type FactoryResumeErrorCode, + type FactoryListRunsOptions, type FactoryRunResult, type FactoryAgentOptions, type RunOptions, @@ -462,6 +463,8 @@ export class CopilotSession { if (options?.resumeFromRunId !== undefined) { return this.factory.resume(options.resumeFromRunId, { limits: options.limits, + notifyOnComplete: options.notifyOnComplete, + logPhaseNames: options.logPhaseNames, }); } const envelope = await this.rpc.factory.run({ @@ -469,6 +472,8 @@ export class CopilotSession { args: options?.args === undefined ? {} : options.args, options: { limits: options?.limits, + notifyOnComplete: options?.notifyOnComplete, + logPhaseNames: options?.logPhaseNames, }, }); @@ -481,6 +486,8 @@ export class CopilotSession { response = await this.rpc.factory.resume({ runId, limits: options?.limits, + notifyOnComplete: options?.notifyOnComplete, + logPhaseNames: options?.logPhaseNames, }); } catch (error) { if ( @@ -499,7 +506,10 @@ export class CopilotSession { }) as SessionFactoryApi["resume"], getRun: async (runId) => this.rpc.factory.getRun({ runId }), waitForRun: (runId, options) => this.waitForFactoryRun(runId, options?.signal), - listRuns: async () => (await this.rpc.factory.listRuns({})).runs, + listRuns: (async (options?: FactoryListRunsOptions) => { + const page = await this.rpc.factory.listRuns(options ?? {}); + return options === undefined ? page.runs : page; + }) as SessionFactoryApi["listRuns"], getRunDetail: (runId) => this.rpc.factory.getRunDetail({ runId }), getRunProgress: (runId, options = {}) => this.rpc.factory.getRunProgress({ runId, ...options }), diff --git a/nodejs/test/e2e/factory.e2e.test.ts b/nodejs/test/e2e/factory.e2e.test.ts index cddd8e47b0..669a1f9c64 100644 --- a/nodejs/test/e2e/factory.e2e.test.ts +++ b/nodejs/test/e2e/factory.e2e.test.ts @@ -84,6 +84,7 @@ it.skipIf(isInProcessTransport)( const result = await session.factory.run("argument-echo", { args: { source: "sdk-e2e", count: 11 }, + notifyOnComplete: false, }); expect(result).toMatchObject({ @@ -140,7 +141,7 @@ it.skipIf(isInProcessTransport)( const { workDir } = factoryTestContext; await using session = await setupFactoryExtension(workDir); - const run = await session.factory.run("argument-echo"); + const run = await session.factory.run("argument-echo", { notifyOnComplete: false }); const error = await session.factory.resume(run.runId).catch((caught: unknown) => caught); expect(error).toBeInstanceOf(FactoryResumeError); @@ -148,6 +149,101 @@ it.skipIf(isInProcessTransport)( } ); +it.skipIf(isInProcessTransport)( + "forwards factory runtime controls across the SDK process boundary", + async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); + } + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const suppressed = await session.factory.run("phased", { + notifyOnComplete: false, + logPhaseNames: false, + }); + + expect(suppressed).toMatchObject({ + status: "completed", + result: "finished", + }); + const progress = await session.factory.getRunProgress(suppressed.runId); + expect(progress.records).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "phase", text: "Collect" }), + expect.objectContaining({ kind: "log", text: "Collected" }), + expect.objectContaining({ kind: "phase", text: "Summarize" }), + expect.objectContaining({ kind: "log", text: "Summarized" }), + ]) + ); + + const events = await session.getEvents(); + expect( + events.some( + (event) => + event.type === "system.notification" && + event.data.kind.type === "factory_completed" && + event.data.kind.runId === suppressed.runId + ) + ).toBe(false); + expect( + events.filter( + (event) => event.type === "session.info" && event.data.infoType === "factory_phase" + ) + ).toEqual([]); + } +); + +it.skipIf(isInProcessTransport)("pages factory runs and returns cursor metadata", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); + } + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const first = await session.factory.run("argument-echo", { + args: { ordinal: 1 }, + notifyOnComplete: false, + }); + const second = await session.factory.run("argument-echo", { + args: { ordinal: 2 }, + notifyOnComplete: false, + }); + const third = await session.factory.run("argument-echo", { + args: { ordinal: 3 }, + notifyOnComplete: false, + }); + + const newest = await session.factory.listRuns({ limit: 1 }); + expect(newest).toMatchObject({ + runs: [expect.objectContaining({ runId: third.runId })], + hasMoreNewer: false, + omittedOlder: 2, + }); + expect(newest.oldestSeq).toBe(newest.newestSeq); + expect(newest.oldestSeq).not.toBeNull(); + + const older = await session.factory.listRuns({ + beforeSeq: newest.oldestSeq!, + limit: 1, + }); + expect(older).toMatchObject({ + runs: [expect.objectContaining({ runId: second.runId })], + hasMoreNewer: true, + omittedOlder: 1, + }); + + const oldest = await session.factory.listRuns({ + beforeSeq: older.oldestSeq!, + limit: 1, + }); + expect(oldest).toMatchObject({ + runs: [expect.objectContaining({ runId: first.runId })], + hasMoreNewer: true, + omittedOlder: 0, + }); +}); + it.skipIf(isInProcessTransport)( "runs a factory when its session denies every permission request", async () => { @@ -158,7 +254,9 @@ it.skipIf(isInProcessTransport)( const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); await using session = await setupFactoryExtension(workDir, denyPermissions); - await expect(session.factory.run("argument-echo")).resolves.toMatchObject({ + await expect( + session.factory.run("argument-echo", { notifyOnComplete: false }) + ).resolves.toMatchObject({ status: "completed", }); expect(denyPermissions).not.toHaveBeenCalled(); @@ -175,12 +273,17 @@ it.skipIf(isInProcessTransport)( const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); await using session = await setupFactoryExtension(workDir, denyPermissions); - const failedRun = await session.factory.run("fails-once"); + const failedRun = await session.factory.run("fails-once", { notifyOnComplete: false }); expect(failedRun).toMatchObject({ status: "error", }); - await expect(session.factory.resume(failedRun.runId)).resolves.toMatchObject({ + await expect( + session.factory.resume(failedRun.runId, { + notifyOnComplete: false, + logPhaseNames: false, + }) + ).resolves.toMatchObject({ status: "completed", result: "resumed", }); @@ -197,7 +300,9 @@ it.skipIf(isInProcessTransport)( const { workDir } = factoryTestContext; await using session = await setupFactoryExtension(workDir); - const result = await session.factory.run("starts-from-context-session"); + const result = await session.factory.run("starts-from-context-session", { + notifyOnComplete: false, + }); expect(result).toMatchObject({ status: "completed", @@ -216,7 +321,9 @@ it.skipIf(isInProcessTransport)( const { workDir } = factoryTestContext; await using session = await setupFactoryExtension(workDir); - const result = await session.factory.run("starts-from-module-session"); + const result = await session.factory.run("starts-from-module-session", { + notifyOnComplete: false, + }); expect(result).toMatchObject({ status: "completed", @@ -236,7 +343,7 @@ it.skipIf(isInProcessTransport)( const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); await using session = await setupFactoryExtension(workDir); - const parked = session.factory.run("parked"); + const parked = session.factory.run("parked", { notifyOnComplete: false }); await retry( "wait for the parked factory to enter its body", async () => { @@ -282,7 +389,7 @@ it.skipIf(isInProcessTransport)( const { workDir } = factoryTestContext; await using session = await setupFactoryExtension(workDir); - const result = await session.factory.run("array-result"); + const result = await session.factory.run("array-result", { notifyOnComplete: false }); expect(result).toMatchObject({ status: "completed", @@ -301,7 +408,10 @@ it.skipIf(isInProcessTransport)( await using session = await setupFactoryExtension(workDir); const args = [1, "two", false]; - const result = await session.factory.run("argument-echo", { args }); + const result = await session.factory.run("argument-echo", { + args, + notifyOnComplete: false, + }); expect(result).toMatchObject({ status: "completed", diff --git a/nodejs/test/e2e/fixtures/factory-extension.mjs b/nodejs/test/e2e/fixtures/factory-extension.mjs index 45227a1bea..fb344b4863 100644 --- a/nodejs/test/e2e/fixtures/factory-extension.mjs +++ b/nodejs/test/e2e/fixtures/factory-extension.mjs @@ -40,6 +40,21 @@ const arrayResult = defineFactory({ run: async () => [1, "two", false], }); +const phased = defineFactory({ + meta: { + name: "phased", + description: "Record named phases and ordinary progress.", + phases: [{ title: "Collect" }, { title: "Summarize" }], + }, + run: async ({ phase, log }) => { + phase("Collect"); + log("Collected"); + phase("Summarize"); + log("Summarized"); + return "finished"; + }, +}); + const forwardsSubagentOptions = defineFactory({ meta: { name: "forwards-subagent-options", @@ -141,6 +156,7 @@ session = await joinSession({ factories: [ argumentEcho, arrayResult, + phased, forwardsSubagentOptions, startsFromContextSession, startsFromModuleSession, @@ -153,6 +169,7 @@ void waitForMarker("start-b", 30_000) .then(async () => { const result = await session.factory.run("argument-echo", { args: { source: "module-watcher" }, + notifyOnComplete: false, }); writeFileSync(marker("b-result"), JSON.stringify({ status: "success", result })); }) diff --git a/nodejs/test/factory.test.ts b/nodejs/test/factory.test.ts index dcf434616a..c9b8f65074 100644 --- a/nodejs/test/factory.test.ts +++ b/nodejs/test/factory.test.ts @@ -435,6 +435,7 @@ describe("factories", () => { const guide = readFileSync(new URL("../docs/factories.md", import.meta.url), "utf8"); const publicApi = readFileSync(new URL("../src/factory.ts", import.meta.url), "utf8"); const listRunsPagingWording = "newest default page of this session's durable factory runs"; + const listRunsMetadata = ["oldestSeq", "newestSeq", "hasMoreNewer", "omittedOlder"]; const resumeCodes = [ "not_found", "non_resumable", @@ -458,6 +459,9 @@ describe("factories", () => { for (const document of [normalizedGuide, normalizedPublicApi]) { expect(document).toContain(listRunsPagingWording); + for (const field of listRunsMetadata) { + expect(document).toContain(field); + } } expect(normalizedGuide).toContain( @@ -1500,14 +1504,31 @@ describe("factories", () => { revision: 4, }; const detail = { ...summary, phases: [], agents: [], progress }; + const runsPage = { + runs: [summary], + oldestSeq: 11, + newestSeq: 12, + hasMoreNewer: true, + omittedOlder: 10, + }; const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.listRuns") return { runs: [summary] }; + if (method === "session.factory.listRuns") return runsPage; if (method === "session.factory.getRunDetail") return detail; return progress; }); const session = new CopilotSession("session-observe", { sendRequest } as never); await expect(session.factory.listRuns()).resolves.toEqual([summary]); + const listedPage = await session.factory.listRuns({ + afterSeq: 10, + beforeSeq: 20, + limit: 50, + }); + expect(listedPage).toEqual(runsPage); + expect(listedPage.oldestSeq).toBe(11); + expect(listedPage.newestSeq).toBe(12); + expect(listedPage.hasMoreNewer).toBe(true); + expect(listedPage.omittedOlder).toBe(10); await expect(session.factory.getRunDetail("run-observe")).resolves.toEqual(detail); await expect( session.factory.getRunProgress("run-observe", { @@ -1519,11 +1540,17 @@ describe("factories", () => { expect(sendRequest).toHaveBeenNthCalledWith(1, "session.factory.listRuns", { sessionId: session.sessionId, }); - expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.getRunDetail", { + expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.listRuns", { + sessionId: session.sessionId, + afterSeq: 10, + beforeSeq: 20, + limit: 50, + }); + expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.getRunDetail", { sessionId: session.sessionId, runId: "run-observe", }); - expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.getRunProgress", { + expect(sendRequest).toHaveBeenNthCalledWith(4, "session.factory.getRunProgress", { sessionId: session.sessionId, runId: "run-observe", phaseId: "p0", @@ -2132,6 +2159,8 @@ describe("factories", () => { await expect( session.factory.resume("run-prior", { limits: { maxTotalSubagents: 7 }, + notifyOnComplete: true, + logPhaseNames: true, }) ).resolves.toMatchObject({ status: "completed", @@ -2141,13 +2170,20 @@ describe("factories", () => { session.factory.run("by-name", { args: { value: 1 }, limits: { maxTotalSubagents: 7 }, + notifyOnComplete: false, + logPhaseNames: true, resumeFromRunId: "run-prior", }) ).resolves.toMatchObject({ status: "completed", result: { name: "stored-name", persistedArgs: true }, }); - await expect(session.factory.run(factory)).resolves.toMatchObject({ + await expect( + session.factory.run(factory, { + notifyOnComplete: true, + logPhaseNames: false, + }) + ).resolves.toMatchObject({ status: "completed", result: { name: "friendly-run" }, }); @@ -2155,17 +2191,25 @@ describe("factories", () => { sessionId: session.sessionId, runId: "run-prior", limits: { maxTotalSubagents: 7 }, + notifyOnComplete: true, + logPhaseNames: true, }); expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.resume", { sessionId: session.sessionId, runId: "run-prior", limits: { maxTotalSubagents: 7 }, + notifyOnComplete: false, + logPhaseNames: true, }); expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.run", { sessionId: session.sessionId, name: "friendly-run", args: {}, - options: { limits: undefined }, + options: { + limits: undefined, + notifyOnComplete: true, + logPhaseNames: false, + }, }); }); From 7a916f8af8fb11315e9d043156487f9076264145 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Mon, 31 Aug 2026 20:18:29 +0000 Subject: [PATCH 07/60] [SDK] Expose Ask User Variant Session Option (#2432) * [SDK] Expose Ask User Variant Session Option Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Skip tool metadata check in-process The in-process transport does not expose current tool metadata for introspection, so keep the runtime schema assertion on the stdio cells where that RPC is supported. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Clarify legacy ask-user handler documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 + dotnet/README.md | 7 +- dotnet/src/Client.cs | 4 + dotnet/src/Types.cs | 31 +++++++- .../test/Unit/ClientSessionLifetimeTests.cs | 48 +++++++++++ dotnet/test/Unit/CloneTests.cs | 2 + go/README.md | 6 +- go/client.go | 15 ++++ go/client_test.go | 79 +++++++++++++++++++ go/types.go | 26 +++++- java/README.md | 5 ++ .../github/copilot/SessionRequestBuilder.java | 2 + .../github/copilot/rpc/AskUserVariant.java | 59 ++++++++++++++ .../copilot/rpc/CreateSessionRequest.java | 13 +++ .../copilot/rpc/ResumeSessionConfig.java | 27 +++++++ .../copilot/rpc/ResumeSessionRequest.java | 13 +++ .../com/github/copilot/rpc/SessionConfig.java | 30 ++++++- .../com/github/copilot/ConfigCloneTest.java | 5 ++ .../copilot/SessionRequestBuilderTest.java | 37 +++++++++ nodejs/README.md | 9 ++- nodejs/src/client.ts | 2 + nodejs/src/index.ts | 1 + nodejs/src/types.ts | 17 +++- nodejs/test/client.test.ts | 33 ++++++++ nodejs/test/e2e/ui_elicitation.e2e.test.ts | 24 ++++++ python/README.md | 5 +- python/copilot/__init__.py | 2 + python/copilot/client.py | 20 +++++ python/copilot/session.py | 2 +- python/test_client.py | 55 +++++++++++++ rust/README.md | 6 ++ rust/src/handler.rs | 7 +- rust/src/types.rs | 54 +++++++++++-- rust/src/wire.rs | 14 ++-- rust/tests/session_test.rs | 58 +++++++++++++- 35 files changed, 692 insertions(+), 30 deletions(-) create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 345308849c..0184a553db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ The experimental Node.js Agent Factories convenience API now supports paginated Factory `run` and `resume` options now accept `notifyOnComplete` and `logPhaseNames`. The SDK forwards these options to the Copilot CLI for new and resumed runs. +### Feature: selectable `ask_user` session behavior + +Session create and cold resume now accept a language-specific `askUserVariant` option with `legacy` and `elicitation` values. SDK sessions retain the legacy question-and-answer tool by default. Select `elicitation` and provide an elicitation handler to expose the structured form-based `ask_user` tool. + ### Feature: rotating session-scoped GitHub credentials All six SDKs can now acquire short-lived GitHub credentials through a session-scoped callback. The SDK registers the callback before session create or resume, maps `initial` and `refresh` requests to the owning session, and removes registrations on rollback, replacement, session close, and client close. Static per-session `gitHubToken` credentials remain supported and are mutually exclusive with the callback. diff --git a/dotnet/README.md b/dotnet/README.md index 461ff0cf94..a6a419644f 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -135,7 +135,8 @@ Create a new conversation session. - `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled. - `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`. - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. -- `OnUserInputRequest` - Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. +- `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. +- `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`. - `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. ##### `ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null): Task` @@ -146,6 +147,7 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section. - `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. +- `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume. ```csharp await using var session = await client.CreateSessionAsync(new SessionConfig @@ -871,7 +873,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `SkipPe ## User Input Requests -Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler: +Enable the legacy question-and-answer `ask_user` tool by providing an `OnUserInputRequest` handler: ```csharp var session = await client.CreateSessionAsync(new SessionConfig @@ -1004,6 +1006,7 @@ var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", OnPermissionRequest = PermissionHandler.ApproveAll, + AskUserVariant = AskUserVariant.Elicitation, OnElicitationRequest = async (context) => { // context.SessionId - Session that triggered the request diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index f2da0a48f7..157e519811 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1190,6 +1190,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.EnableCitations, config.EnableFileChangeTracking, wireSystemMessage, + config.AskUserVariant, toolFilter.AvailableTools, toolFilter.ExcludedTools, config.ExcludedBuiltInAgents, @@ -1428,6 +1429,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.EnableCitations, config.EnableFileChangeTracking, wireSystemMessage, + config.AskUserVariant, toolFilter.AvailableTools, toolFilter.ExcludedTools, config.ExcludedBuiltInAgents, @@ -2868,6 +2870,7 @@ internal record CreateSessionRequest( bool? EnableCitations, bool? EnableFileChangeTracking, SystemMessageConfig? SystemMessage, + AskUserVariant? AskUserVariant, IList? AvailableTools, IList? ExcludedTools, [property: JsonPropertyName("excludedBuiltinAgents")] IList? ExcludedBuiltInAgents, @@ -2983,6 +2986,7 @@ internal record ResumeSessionRequest( bool? EnableCitations, bool? EnableFileChangeTracking, SystemMessageConfig? SystemMessage, + AskUserVariant? AskUserVariant, IList? AvailableTools, IList? ExcludedTools, [property: JsonPropertyName("excludedBuiltinAgents")] IList? ExcludedBuiltInAgents, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 6ed05e3064..6cf02d2a73 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -3116,6 +3116,21 @@ public sealed class ManagedSettings public ManagedSettingsPermissions? Permissions { get; set; } } +/// +/// Selects the model-facing shape of the built-in ask_user tool. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AskUserVariant +{ + /// Use the legacy user-input request flow. + [JsonStringEnumMemberName("legacy")] + Legacy, + + /// Use the elicitation request flow. + [JsonStringEnumMemberName("elicitation")] + Elicitation +} + /// /// Shared configuration properties for creating or resuming a Copilot session. /// Use when creating a new session, or @@ -3205,6 +3220,7 @@ protected SessionConfigBase(SessionConfigBase? other) ReasoningEffort = other.ReasoningEffort; ReasoningSummary = other.ReasoningSummary; ContextTier = other.ContextTier; + AskUserVariant = other.AskUserVariant; CreateSessionFsProvider = other.CreateSessionFsProvider; GitHubToken = other.GitHubToken; GitHubTokenProvider = other.GitHubTokenProvider; @@ -3372,6 +3388,15 @@ protected SessionConfigBase(SessionConfigBase? other) /// System message configuration for the session. public SystemMessageConfig? SystemMessage { get; set; } + /// + /// Selects the model-facing shape of the built-in ask_user tool. + /// The default is . To use + /// , also provide + /// so the host can answer structured forms. + /// The runtime resolves this option when it creates or cold-resumes the session. + /// + public AskUserVariant? AskUserVariant { get; set; } + /// List of tool names to allow; only these tools will be available when specified. public IList? AvailableTools { get; set; } @@ -3470,7 +3495,11 @@ protected SessionConfigBase(SessionConfigBase? other) /// Handler for permission requests from the server. public Func>? OnPermissionRequest { get; set; } - /// Handler for user input requests from the agent. + /// + /// Handler for user input requests from the agent. When provided with the default + /// variant, enables the + /// question-and-answer form of the ask_user tool. + /// public Func>? OnUserInputRequest { get; set; } /// Slash commands registered for this session. diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index b61546c650..bb0042efdb 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -464,6 +464,54 @@ public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unse Assert.False(agent.TryGetProperty("reasoningEffort", out _)); } + [Fact] + public async Task CreateSessionAsync_Forwards_AskUserVariant() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + + await using var session = await client.CreateSessionAsync(new SessionConfig + { + AskUserVariant = AskUserVariant.Elicitation, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + Assert.Equal("elicitation", request.Params.GetProperty("askUserVariant").GetString()); + + server.ClearRequests(); + await using var defaultSession = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + var defaultRequest = Assert.Single(server.Requests, request => request.Method == "session.create"); + Assert.False(defaultRequest.Params.TryGetProperty("askUserVariant", out _)); + } + + [Fact] + public async Task ResumeSessionAsync_Forwards_AskUserVariant_On_Cold_Resume() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + + await using var session = await client.ResumeSessionAsync("ask-user-variant", new ResumeSessionConfig + { + AskUserVariant = AskUserVariant.Legacy, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.resume"); + Assert.Equal("legacy", request.Params.GetProperty("askUserVariant").GetString()); + + server.ClearRequests(); + await using var defaultSession = await client.ResumeSessionAsync("ask-user-variant-default", new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + var defaultRequest = Assert.Single(server.Requests, request => request.Method == "session.resume"); + Assert.False(defaultRequest.Params.TryGetProperty("askUserVariant", out _)); + } + [Fact] public async Task SessionRequests_Serialize_AdditionalDirectories() { diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 4bacdfe33d..2948ac2129 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -73,6 +73,7 @@ public void SessionConfig_Clone_CopiesAllProperties() ReasoningEffort = "high", ReasoningSummary = ReasoningSummary.Detailed, ContextTier = ContextTier.LongContext, + AskUserVariant = AskUserVariant.Elicitation, ConfigDirectory = "/config", AvailableTools = ["tool1", "tool2"], ExcludedTools = ["tool3"], @@ -121,6 +122,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.ReasoningEffort, clone.ReasoningEffort); Assert.Equal(original.ReasoningSummary, clone.ReasoningSummary); Assert.Equal(original.ContextTier, clone.ContextTier); + Assert.Equal(original.AskUserVariant, clone.AskUserVariant); Assert.Equal(original.ConfigDirectory, clone.ConfigDirectory); Assert.Equal(original.AvailableTools, clone.AvailableTools); Assert.Equal(original.ExcludedTools, clone.ExcludedTools); diff --git a/go/README.md b/go/README.md index ddd74b91aa..3d8706cec1 100644 --- a/go/README.md +++ b/go/README.md @@ -224,7 +224,8 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `EnableSessionStore` (\*bool): Enables the cross-session store for search and retrieval across sessions. When unset in `ModeCopilotCli`, the runtime default applies (enabled). In `ModeEmpty`, defaults to disabled. - `GitHubTokenProvider` (GitHubTokenProvider): Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenResult` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenCancelled`. Cannot be combined with `GitHubToken`. - `OnPermissionRequest` (PermissionHandlerFunc): Optional handler called before each tool execution to approve or deny it. When nil, permission requests are emitted as events and left pending for manual resolution. `copilot.PermissionHandler.ApproveAll` approves requests when managed settings are disabled and returns an error when `EnableManagedSettings` is true. Custom handlers can inspect `RequiresManagedApproval()` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. -- `OnUserInputRequest` (UserInputHandler): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. +- `OnUserInputRequest` (UserInputHandler): Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. +- `AskUserVariant` (AskUserVariant): Selects the model-facing shape of the `ask_user` tool. The zero value preserves legacy behavior; use `AskUserVariantElicitation` with `OnElicitationRequest`. - `Hooks` (\*SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. - `Commands` ([]CommandDefinition): Slash-commands registered for this session. See [Commands](#commands) section. - `OnElicitationRequest` (ElicitationHandler): Handler for elicitation requests from the server. See [Elicitation Requests](#elicitation-requests-serverclient) section. @@ -238,6 +239,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `Streaming` (*bool): Enable streaming delta events (nil = runtime default) - `Commands` ([]CommandDefinition): Slash-commands. See [Commands](#commands) section. - `OnElicitationRequest` (ElicitationHandler): Elicitation handler. See [Elicitation Requests](#elicitation-requests-serverclient) section. +- `AskUserVariant` (AskUserVariant): Selects the model-facing shape of the `ask_user` tool on cold resume. Re-supply `AskUserVariantElicitation` with `OnElicitationRequest`; the zero value preserves legacy behavior. - `GitHubTokenProvider` (GitHubTokenProvider): Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. ```go @@ -770,7 +772,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `SkipPe ## User Input Requests -Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler: +Enable the legacy question-and-answer `ask_user` tool by providing an `OnUserInputRequest` handler: ```go session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ diff --git a/go/client.go b/go/client.go index 4e44696a55..2dbd7f7e95 100644 --- a/go/client.go +++ b/go/client.go @@ -788,6 +788,13 @@ func hasManagedSettings(enableManagedSettings *bool, managedSettings *ManagedSet return (enableManagedSettings != nil && *enableManagedSettings) || managedSettings != nil } +func validateAskUserVariant(variant AskUserVariant) error { + if variant != "" && variant != AskUserVariantLegacy && variant != AskUserVariantElicitation { + return fmt.Errorf("invalid AskUserVariant %q: expected %q, %q, or unset", variant, AskUserVariantLegacy, AskUserVariantElicitation) + } + return nil +} + func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Session, error) { if config == nil { config = &SessionConfig{} @@ -795,6 +802,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.GitHubToken != "" && config.GitHubTokenProvider != nil { return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together") } + if err := validateAskUserVariant(config.AskUserVariant); err != nil { + return nil, err + } if err := c.ensureConnected(ctx); err != nil { return nil, err @@ -842,6 +852,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.Capi = config.Capi req.Providers = config.Providers req.Models = config.Models + req.AskUserVariant = config.AskUserVariant req.EnableSessionTelemetry = config.EnableSessionTelemetry req.EnableCitations = config.EnableCitations req.EnableFileChangeTracking = config.EnableFileChangeTracking @@ -1170,6 +1181,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.GitHubToken != "" && config.GitHubTokenProvider != nil { return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together") } + if err := validateAskUserVariant(config.AskUserVariant); err != nil { + return nil, err + } if err := c.ensureConnected(ctx); err != nil { return nil, err @@ -1200,6 +1214,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.Capi = config.Capi req.Providers = config.Providers req.Models = config.Models + req.AskUserVariant = config.AskUserVariant req.EnableSessionTelemetry = config.EnableSessionTelemetry req.IsExperimentalMode = config.EnableExperimentalMode req.SkipCustomInstructions = config.SkipCustomInstructions diff --git a/go/client_test.go b/go/client_test.go index c6ab0808cb..b701de437f 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -445,6 +445,85 @@ func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { assertCapiEnableWebSocketResponses(t, <-resumeParams) } +func TestClient_ForwardsAskUserVariantToSessionRequests(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + createParams := make(chan json.RawMessage, 2) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + resumeParams := make(chan json.RawMessage, 2) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: "ask-user-create", + AskUserVariant: AskUserVariantElicitation, + }); err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), "ask-user-cold-resume", &ResumeSessionConfig{ + AskUserVariant: AskUserVariantLegacy, + }); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + if _, err := client.CreateSession(t.Context(), &SessionConfig{SessionID: "ask-user-default-create"}); err != nil { + t.Fatalf("CreateSession with default failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), "ask-user-default-cold-resume", nil); err != nil { + t.Fatalf("ResumeSession with default failed: %v", err) + } + + assertAskUserVariant(t, <-createParams, "elicitation") + assertAskUserVariant(t, <-resumeParams, "legacy") + assertAskUserVariant(t, <-createParams, "") + assertAskUserVariant(t, <-resumeParams, "") +} + +func TestClient_RejectsInvalidAskUserVariant(t *testing.T) { + client := &Client{} + + if _, err := client.CreateSession(t.Context(), &SessionConfig{ + AskUserVariant: AskUserVariant("unknown"), + }); err == nil || !strings.Contains(err.Error(), "AskUserVariant") { + t.Fatalf("CreateSession error = %v, want invalid AskUserVariant error", err) + } + if _, err := client.ResumeSession(t.Context(), "cold-resume", &ResumeSessionConfig{ + AskUserVariant: AskUserVariant("unknown"), + }); err == nil || !strings.Contains(err.Error(), "AskUserVariant") { + t.Fatalf("ResumeSession error = %v, want invalid AskUserVariant error", err) + } +} + +func assertAskUserVariant(t *testing.T, params json.RawMessage, want string) { + t.Helper() + var payload map[string]any + if err := json.Unmarshal(params, &payload); err != nil { + t.Fatalf("failed to decode request params: %v", err) + } + got, present := payload["askUserVariant"] + if want == "" { + if present { + t.Fatalf("askUserVariant = %v, want omitted", got) + } + return + } + if got != want { + t.Fatalf("askUserVariant = %v, want %q", got, want) + } +} + func TestClient_ForwardsAdditionalDirectoriesToSessionRequests(t *testing.T) { rpcClient, server, _ := newRuntimeShutdownRpcPair(t) t.Cleanup(server.Stop) diff --git a/go/types.go b/go/types.go index 1d98e06158..a574168aba 100644 --- a/go/types.go +++ b/go/types.go @@ -1251,6 +1251,16 @@ type GitHubMCPToolConfig struct { DisableFormDeferral *bool `json:"disableFormDeferral,omitempty"` } +// AskUserVariant selects the model-facing shape of the ask_user tool. +type AskUserVariant string + +const ( + // AskUserVariantLegacy uses the legacy user-input request implementation. + AskUserVariantLegacy AskUserVariant = "legacy" + // AskUserVariantElicitation uses the elicitation-based implementation. + AskUserVariantElicitation AskUserVariant = "elicitation" +) + // SessionConfig configures a new session type SessionConfig struct { // SessionID is an optional custom session ID @@ -1340,8 +1350,13 @@ type SessionConfig struct { // GitHubTokenProvider acquires session-scoped GitHub tokens on demand. It // cannot be combined with GitHubToken. GitHubTokenProvider GitHubTokenProvider - // OnUserInputRequest is a handler for user input requests from the agent (enables ask_user tool) + // OnUserInputRequest handles legacy question-and-answer requests from the agent + // and enables the legacy ask_user tool. OnUserInputRequest UserInputHandler + // AskUserVariant selects the model-facing shape of the ask_user tool. + // The zero value preserves legacy behavior. AskUserVariantElicitation also + // requires OnElicitationRequest so the host can answer structured forms. + AskUserVariant AskUserVariant // Hooks configures hook handlers for session lifecycle events Hooks *SessionHooks // WorkingDirectory is the working directory for the session. @@ -1914,8 +1929,13 @@ type ResumeSessionConfig struct { // 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 handles legacy question-and-answer requests from the agent + // and enables the legacy ask_user tool. OnUserInputRequest UserInputHandler + // AskUserVariant selects the model-facing shape of the ask_user tool. + // The zero value preserves legacy behavior. AskUserVariantElicitation also + // requires OnElicitationRequest so the host can answer structured forms. + AskUserVariant AskUserVariant // Hooks configures hook handlers for session lifecycle events Hooks *SessionHooks // WorkingDirectory is the working directory for the session. @@ -2509,6 +2529,7 @@ type createSessionRequest struct { ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` RequestPermission *bool `json:"requestPermission,omitempty"` RequestUserInput *bool `json:"requestUserInput,omitempty"` + AskUserVariant AskUserVariant `json:"askUserVariant,omitempty"` RequestExitPlanMode *bool `json:"requestExitPlanMode,omitempty"` RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` Hooks *bool `json:"hooks,omitempty"` @@ -2606,6 +2627,7 @@ type resumeSessionRequest struct { ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` RequestPermission *bool `json:"requestPermission,omitempty"` RequestUserInput *bool `json:"requestUserInput,omitempty"` + AskUserVariant AskUserVariant `json:"askUserVariant,omitempty"` RequestExitPlanMode *bool `json:"requestExitPlanMode,omitempty"` RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` Hooks *bool `json:"hooks,omitempty"` diff --git a/java/README.md b/java/README.md index 2e1ba51812..e64964255b 100644 --- a/java/README.md +++ b/java/README.md @@ -176,6 +176,11 @@ directly. `CopilotClientOptions.setCwd(...)` sets the runtime process working directory, which otherwise inherits the current process working directory. `SessionConfig.setWorkingDirectory(...)` sets the session working directory, which otherwise defaults to the runtime process working directory. +`SessionConfig.setAskUserVariant(AskUserVariant.ELICITATION)` selects the +structured form-based `ask_user` tool when an elicitation handler is also set. +The default is `AskUserVariant.LEGACY`. Re-supply the option and handler through +`ResumeSessionConfig` on a cold resume. + For rotating per-session GitHub credentials, use `SessionConfig.setGitHubTokenProvider(...)` (or the equivalent `ResumeSessionConfig` setter) instead of `setGitHubToken(...)`: diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index 4254c04ec4..e0a8655390 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -119,6 +119,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setReasoningEffort(config.getReasoningEffort()); request.setReasoningSummary(config.getReasoningSummary()); request.setContextTier(config.getContextTier()); + request.setAskUserVariant(config.getAskUserVariant()); request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); @@ -255,6 +256,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setReasoningEffort(config.getReasoningEffort()); request.setReasoningSummary(config.getReasoningSummary()); request.setContextTier(config.getContextTier()); + request.setAskUserVariant(config.getAskUserVariant()); request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java b/java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java new file mode 100644 index 0000000000..17c6a1333f --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Selects how the built-in {@code ask_user} tool collects user input. + */ +public enum AskUserVariant { + + /** Uses the legacy question-and-answer experience. */ + LEGACY("legacy"), + + /** Uses structured elicitation to collect user input. */ + ELICITATION("elicitation"); + + private final String value; + + AskUserVariant(String value) { + this.value = value; + } + + /** + * Returns the wire-format value. + * + * @return the value used in JSON serialization + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Creates an {@code AskUserVariant} from its wire-format value. + * + * @param value + * the wire-format value + * @return the matching variant, or {@code null} when {@code value} is + * {@code null} + * @throws IllegalArgumentException + * if the value is not {@code legacy} or {@code elicitation} + */ + @JsonCreator + public static AskUserVariant fromValue(String value) { + if (value == null) { + return null; + } + for (AskUserVariant variant : values()) { + if (variant.value.equals(value)) { + return variant; + } + } + throw new IllegalArgumentException("Unknown AskUserVariant value: " + value); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index 403893987d..ef3531fa5f 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -45,6 +45,9 @@ public final class CreateSessionRequest { @JsonProperty("contextTier") private String contextTier; + @JsonProperty("askUserVariant") + private AskUserVariant askUserVariant; + @JsonProperty("tools") private List tools; @@ -309,6 +312,16 @@ public void setContextTier(String contextTier) { this.contextTier = contextTier; } + /** Gets the ask-user variant. @return the ask-user variant */ + public AskUserVariant getAskUserVariant() { + return askUserVariant; + } + + /** Sets the ask-user variant. @param askUserVariant the ask-user variant */ + public void setAskUserVariant(AskUserVariant askUserVariant) { + this.askUserVariant = askUserVariant; + } + /** Gets the tools. @return the tool definitions */ public List getTools() { return tools == null ? null : Collections.unmodifiableList(tools); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index a55c3454e7..d13b67d715 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -63,6 +63,7 @@ public class ResumeSessionConfig { private String reasoningEffort; private String reasoningSummary; private String contextTier; + private AskUserVariant askUserVariant; private ModelCapabilitiesOverride modelCapabilities; private PermissionHandler onPermissionRequest; private McpAuthHandler onMcpAuthRequest; @@ -793,6 +794,31 @@ public ResumeSessionConfig setContextTier(String contextTier) { return this; } + /** + * Gets the experience used by the built-in {@code ask_user} tool. + * + * @return the ask-user variant, or {@code null} to use the legacy experience + */ + public AskUserVariant getAskUserVariant() { + return askUserVariant; + } + + /** + * Sets the model-facing shape of the built-in {@code ask_user} tool when the + * session is resumed by a new client. + *

+ * When unset, the option is omitted and the legacy shape is used. Set an + * elicitation handler when selecting {@link AskUserVariant#ELICITATION}. + * + * @param askUserVariant + * the ask-user variant + * @return this config instance for method chaining + */ + public ResumeSessionConfig setAskUserVariant(AskUserVariant askUserVariant) { + this.askUserVariant = askUserVariant; + return this; + } + /** * Gets the permission request handler. * @@ -2052,6 +2078,7 @@ public ResumeSessionConfig clone() { copy.reasoningEffort = this.reasoningEffort; copy.reasoningSummary = this.reasoningSummary; copy.contextTier = this.contextTier; + copy.askUserVariant = this.askUserVariant; copy.modelCapabilities = this.modelCapabilities; copy.onPermissionRequest = this.onPermissionRequest; copy.onUserInputRequest = this.onUserInputRequest; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 9b8e897fda..99e4090438 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -47,6 +47,9 @@ public final class ResumeSessionRequest { @JsonProperty("contextTier") private String contextTier; + @JsonProperty("askUserVariant") + private AskUserVariant askUserVariant; + @JsonProperty("tools") private List tools; @@ -311,6 +314,16 @@ public void setContextTier(String contextTier) { this.contextTier = contextTier; } + /** Gets the ask-user variant. @return the ask-user variant */ + public AskUserVariant getAskUserVariant() { + return askUserVariant; + } + + /** Sets the ask-user variant. @param askUserVariant the ask-user variant */ + public void setAskUserVariant(AskUserVariant askUserVariant) { + this.askUserVariant = askUserVariant; + } + /** Gets the tools. @return the tool definitions */ public List getTools() { return tools == null ? null : Collections.unmodifiableList(tools); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index 9f6ddb5efa..cc2a858f68 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -46,6 +46,7 @@ public class SessionConfig { private String reasoningEffort; private String reasoningSummary; private String contextTier; + private AskUserVariant askUserVariant; private List tools; private SystemMessageConfig systemMessage; private List availableTools; @@ -254,6 +255,30 @@ public SessionConfig setContextTier(String contextTier) { return this; } + /** + * Gets the experience used by the built-in {@code ask_user} tool. + * + * @return the ask-user variant, or {@code null} to use the legacy experience + */ + public AskUserVariant getAskUserVariant() { + return askUserVariant; + } + + /** + * Sets the model-facing shape of the built-in {@code ask_user} tool. + *

+ * When unset, the option is omitted and the legacy shape is used. Set an + * elicitation handler when selecting {@link AskUserVariant#ELICITATION}. + * + * @param askUserVariant + * the ask-user variant + * @return this config instance for method chaining + */ + public SessionConfig setAskUserVariant(AskUserVariant askUserVariant) { + this.askUserVariant = askUserVariant; + return this; + } + /** * Gets the custom tools for this session. * @@ -894,7 +919,9 @@ public UserInputHandler getOnUserInputRequest() { /** * Sets a handler for user input requests from the agent. *

- * When provided, enables the ask_user tool for the agent to request user input. + * When provided, enables the legacy question-and-answer form of the + * {@code ask_user} tool. Use an elicitation handler with + * {@link AskUserVariant#ELICITATION}. * * @param onUserInputRequest * the user input handler @@ -2173,6 +2200,7 @@ public SessionConfig clone() { copy.reasoningEffort = this.reasoningEffort; copy.reasoningSummary = this.reasoningSummary; copy.contextTier = this.contextTier; + copy.askUserVariant = this.askUserVariant; copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; copy.systemMessage = this.systemMessage; copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; diff --git a/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java b/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java index 4c5a3fbef0..2433e5f67a 100644 --- a/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java @@ -18,6 +18,7 @@ import com.github.copilot.generated.SessionEvent; import com.github.copilot.generated.rpc.SessionLimitsConfig; import com.github.copilot.rpc.AutoModeSwitchResponse; +import com.github.copilot.rpc.AskUserVariant; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.DefaultAgentConfig; import com.github.copilot.rpc.ExitPlanModeResult; @@ -119,6 +120,7 @@ void sessionConfigCloneBasic() { original.setModel("gpt-4o"); original.setReasoningSummary("detailed"); original.setContextTier("long_context"); + original.setAskUserVariant(AskUserVariant.ELICITATION); original.setPluginDirectories(List.of("/plugins/a", "/plugins/b")); original.setDisabledMcpServers(List.of("local-files", "remote-github")); original.setLargeOutput( @@ -133,6 +135,7 @@ void sessionConfigCloneBasic() { assertEquals(original.getModel(), cloned.getModel()); assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary()); assertEquals(original.getContextTier(), cloned.getContextTier()); + assertEquals(original.getAskUserVariant(), cloned.getAskUserVariant()); assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories()); assertEquals(original.getDisabledMcpServers(), cloned.getDisabledMcpServers()); assertEquals(original.getLargeOutput(), cloned.getLargeOutput()); @@ -198,6 +201,7 @@ void resumeSessionConfigCloneBasic() { original.setModel("o1"); original.setReasoningSummary("none"); original.setContextTier("long_context"); + original.setAskUserVariant(AskUserVariant.LEGACY); original.setPluginDirectories(List.of("/plugins/r")); original.setDisabledMcpServers(List.of("local-files-r")); original.setLargeOutput( @@ -210,6 +214,7 @@ void resumeSessionConfigCloneBasic() { assertEquals(original.getModel(), cloned.getModel()); assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary()); assertEquals(original.getContextTier(), cloned.getContextTier()); + assertEquals(original.getAskUserVariant(), cloned.getAskUserVariant()); assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories()); assertEquals(original.getDisabledMcpServers(), cloned.getDisabledMcpServers()); assertEquals(original.getLargeOutput(), cloned.getLargeOutput()); diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 9d76d18ee2..5ea8c69274 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test; import com.github.copilot.generated.rpc.SessionLimitsConfig; +import com.github.copilot.rpc.AskUserVariant; import com.github.copilot.rpc.AutoModeSwitchResponse; import com.github.copilot.rpc.CloudSessionOptions; import com.github.copilot.rpc.CloudSessionRepository; @@ -78,6 +79,42 @@ void testGitHubTokenProviderResultRedactsToken() { assertFalse(result.toString().contains("do-not-print")); } + @Test + void askUserVariantIsForwardedAndSerializedForCreateAndColdResume() throws Exception { + var createRequest = SessionRequestBuilder.buildCreateRequest( + new SessionConfig().setAskUserVariant(AskUserVariant.ELICITATION), "create-session"); + var resumeRequest = SessionRequestBuilder.buildResumeRequest("resume-session", + new ResumeSessionConfig().setAskUserVariant(AskUserVariant.LEGACY)); + var mapper = JsonRpcClient.getObjectMapper(); + + assertEquals(AskUserVariant.ELICITATION, createRequest.getAskUserVariant()); + assertEquals("elicitation", + mapper.readTree(mapper.writeValueAsBytes(createRequest)).path("askUserVariant").asText()); + assertEquals(AskUserVariant.LEGACY, resumeRequest.getAskUserVariant()); + assertEquals("legacy", + mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).path("askUserVariant").asText()); + } + + @Test + void askUserVariantDefaultsToOmittedLegacyBehavior() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var createRequest = SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "create-session"); + var resumeRequest = SessionRequestBuilder.buildResumeRequest("resume-session", new ResumeSessionConfig()); + + assertNull(createRequest.getAskUserVariant()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(createRequest)).has("askUserVariant")); + assertNull(resumeRequest.getAskUserVariant()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).has("askUserVariant")); + } + + @Test + void askUserVariantAcceptsOnlySupportedWireValues() { + assertEquals(AskUserVariant.LEGACY, AskUserVariant.fromValue("legacy")); + assertEquals(AskUserVariant.ELICITATION, AskUserVariant.fromValue("elicitation")); + assertThrows(IllegalArgumentException.class, () -> AskUserVariant.fromValue("ELICITATION")); + assertThrows(IllegalArgumentException.class, () -> AskUserVariant.fromValue("unsupported")); + } + @Test void testBuildCreateRequestHooksNonNullButEmpty() { // Hooks object exists but hasHooks() returns false diff --git a/nodejs/README.md b/nodejs/README.md index 93f9c3fa6b..53e81c7aa4 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -140,7 +140,8 @@ Create a new conversation session. - `gitHubTokenProvider?: GitHubTokenProvider` - Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `gitHubToken`. - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section. - `onPermissionRequest?: PermissionHandler` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `approveAll` approves requests when managed settings are disabled and throws when `enableManagedSettings` is true. Custom handlers can inspect `managedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. -- `onUserInputRequest?: UserInputHandler` - Handler for user input requests from the agent. Enables the `ask_user` tool. See [User Input Requests](#user-input-requests) section. +- `onUserInputRequest?: UserInputHandler` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. +- `askUserVariant?: "legacy" | "elicitation"` - Selects the model-facing `ask_user` tool shape when creating or cold-resuming a session. Defaults to `"legacy"`; use `"elicitation"` with `onElicitationRequest`. - `onElicitationRequest?: ElicitationHandler` - Handler for elicitation requests dispatched by the server. Enables this client to present form-based UI dialogs on behalf of the agent or other session participants. See [Elicitation Requests](#elicitation-requests) section. - `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. @@ -959,7 +960,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `skipPe ## User Input Requests -Enable the agent to ask questions to the user using the `ask_user` tool by providing an `onUserInputRequest` handler: +Enable the legacy question-and-answer `ask_user` tool by providing an `onUserInputRequest` handler: ```typescript const session = await client.createSession({ @@ -991,6 +992,7 @@ Register an `onElicitationRequest` handler to let your client act as an elicitat const session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll, + askUserVariant: "elicitation", onElicitationRequest: async (context) => { // context.sessionId - Session that triggered the request // context.message - Description of what information is needed @@ -1012,6 +1014,9 @@ const session = await client.createSession({ console.log(session.capabilities.ui?.elicitation); // true ``` +Set `askUserVariant: "elicitation"` to expose the structured form as the model's +`ask_user` tool. Omit it to retain the legacy SDK behavior. + When `onElicitationRequest` is provided, the SDK sends `requestElicitation: true` during session create/resume, which enables `session.capabilities.ui.elicitation` on the session. In multi-client scenarios: diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 9b853aa597..13bb9c904e 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1678,6 +1678,7 @@ export class CopilotClient { requestPermission: !!config.onPermissionRequest, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, + askUserVariant: config.askUserVariant, ...(config.enableMcpApps ? { requestMcpApps: true } : {}), ...(config.githubMcpToolConfig != null ? { githubMcpToolConfig: config.githubMcpToolConfig } @@ -1946,6 +1947,7 @@ export class CopilotClient { config.onPermissionRequest !== defaultJoinSessionPermissionHandler, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, + askUserVariant: config.askUserVariant, ...(config.enableMcpApps ? { requestMcpApps: true } : {}), ...(config.githubMcpToolConfig != null ? { githubMcpToolConfig: config.githubMcpToolConfig } diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 1f4359ff8c..8b9095e0ba 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -53,6 +53,7 @@ export { // surface for those six identifiers is preserved unchanged. export type * from "./generated/session-events.js"; export type { + AskUserVariant, CommandContext, CommandDefinition, CommandHandler, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 616e15a467..e6f90490ed 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1265,7 +1265,7 @@ export const defaultJoinSessionPermissionHandler: PermissionHandler = // ============================================================================ /** - * Request for user input from the agent (enables ask_user tool) + * Legacy question-and-answer request from the `ask_user` tool. */ export interface UserInputRequest { /** @@ -2245,6 +2245,9 @@ export interface ManagedSettings { permissions?: ManagedSettingsPermissions; } +/** Selects the model-facing shape of the built-in `ask_user` tool. */ +export type AskUserVariant = "legacy" | "elicitation"; + /** * Shared configuration fields used by both {@link SessionConfig} (for * creating a new session) and {@link ResumeSessionConfig} (for resuming @@ -2556,10 +2559,20 @@ export interface SessionConfigBase { /** * Handler for user input requests from the agent. - * When provided, enables the ask_user tool allowing the agent to ask questions. + * When provided with the default `legacy` {@link AskUserVariant}, enables the + * question-and-answer form of the `ask_user` tool. */ onUserInputRequest?: UserInputHandler; + /** + * Selects the model-facing shape of the built-in `ask_user` tool. + * + * The default is `"legacy"`. To use `"elicitation"`, also provide + * {@link onElicitationRequest} so the host can answer structured forms. + * The runtime resolves this option when it creates or cold-resumes the session. + */ + askUserVariant?: AskUserVariant; + /** * Handler for elicitation requests from the agent. * When provided, the server calls back to this client for form-based UI dialogs. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 3ffda2fa71..5baff82cb4 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -303,6 +303,39 @@ describe("CopilotClient", () => { }); }); + it("forwards the ask-user variant on create and cold resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + const onElicitationRequest = async () => ({ action: "decline" as const }); + + const session = await client.createSession({ + askUserVariant: "elicitation", + onElicitationRequest, + }); + await client.resumeSession(session.sessionId, { + askUserVariant: "elicitation", + onElicitationRequest, + }); + + expect(spy.mock.calls.find(([method]) => method === "session.create")![1]).toMatchObject({ + askUserVariant: "elicitation", + requestElicitation: true, + }); + expect(spy.mock.calls.find(([method]) => method === "session.resume")![1]).toMatchObject({ + askUserVariant: "elicitation", + requestElicitation: true, + }); + }); + it("omits GitHub MCP tool config when unset", async () => { const client = new CopilotClient(); await client.start(); diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts index 2e85dd5af2..ad2adab109 100644 --- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts +++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts @@ -38,6 +38,30 @@ describe("UI Elicitation Callback", async () => { } ); + // In-process sessions do not expose current tool metadata for introspection. + it.skipIf(isInProcessTransport)( + "session created with the elicitation ask-user variant exposes the structured tool", + { timeout: 60_000 }, + async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + askUserVariant: "elicitation", + onElicitationRequest: async () => ({ action: "accept", content: {} }), + }); + + await session.rpc.tools.initializeAndValidate(); + const { tools } = await session.rpc.tools.getCurrentMetadata(); + const askUserSchema = tools?.find((tool) => tool.name === "ask_user")?.input_schema as + | { properties?: Record } + | undefined; + + expect(askUserSchema?.properties).toHaveProperty("message"); + expect(askUserSchema?.properties).toHaveProperty("requestedSchema"); + expect(askUserSchema?.properties).not.toHaveProperty("question"); + await session.disconnect(); + } + ); + it( "session created without onElicitationRequest reports no elicitation capability", { timeout: 60_000 }, diff --git a/python/README.md b/python/README.md index 61608c16a0..c130f21c65 100644 --- a/python/README.md +++ b/python/README.md @@ -283,7 +283,8 @@ These are passed as keyword arguments to `create_session()`: - `enable_session_store` (bool): Enables the cross-session store for search and retrieval across sessions. When unset in `"copilot-cli"` mode, the runtime default applies (enabled). In `"empty"` mode, defaults to disabled. - `github_token_provider` (callable): Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `github_token`. - `on_permission_request` (callable): Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.approve_all` approves requests when managed settings are disabled and raises an error when `enable_managed_settings` is true. Custom handlers can inspect `managed_approval_required` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. -- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. +- `on_user_input_request` (callable): Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. +- `ask_user_variant` (`"legacy"` | `"elicitation"`): Selects the model-facing shape of the `ask_user` tool. Defaults to `"legacy"`; use `"elicitation"` with `on_elicitation_request`. Re-supply this option when cold-resuming a session. - `hooks` (SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. ```python @@ -899,7 +900,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `skip_p ## User Input Requests -Enable the agent to ask questions to the user using the `ask_user` tool by providing an `on_user_input_request` handler: +Enable the legacy question-and-answer `ask_user` tool by providing an `on_user_input_request` handler: ```python async def handle_user_input(request, invocation): diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 608dacf253..4b3b9901ed 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -29,6 +29,7 @@ OpenCanvasInstance, ) from .client import ( + AskUserVariant, CapiSessionOptions, ChildProcessRuntimeConnection, CloudSessionOptions, @@ -229,6 +230,7 @@ "AutoModeSwitchHandler", "AutoModeSwitchRequest", "AutoModeSwitchResponse", + "AskUserVariant", "BUILTIN_TOOLS_ISOLATED", "CanvasAction", "CanvasDeclaration", diff --git a/python/copilot/client.py b/python/copilot/client.py index 271fad626c..05412246de 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -177,6 +177,8 @@ class GitHubTokenCancelledResult(TypedDict): _ConnectionState = Literal["disconnected", "connecting", "connected", "error"] LogLevel = Literal["none", "error", "warning", "info", "debug", "all"] +AskUserVariant = Literal["legacy", "elicitation"] +"""Model-facing shape of the runtime's built-in ``ask_user`` tool.""" @dataclass @@ -2209,6 +2211,7 @@ async def create_session( available_tools: list[str] | ToolSet | None = None, excluded_tools: list[str] | ToolSet | None = None, on_user_input_request: UserInputHandler | None = None, + ask_user_variant: AskUserVariant | None = None, hooks: SessionHooks | None = None, working_directory: str | None = None, additional_directories: list[str] | None = None, @@ -2311,6 +2314,10 @@ async def create_session( including custom tools registered via ``tools=``. Ignored if ``available_tools`` is set. on_user_input_request: Handler for user input requests. + ask_user_variant: Model-facing shape of the ``ask_user`` tool. + Accepted values are ``"legacy"`` and ``"elicitation"``. The + default is ``"legacy"``. To use ``"elicitation"``, also provide + ``on_elicitation_request`` so the host can answer structured forms. hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. @@ -2465,6 +2472,8 @@ async def create_session( raise ValueError("on_permission_request must be callable when provided.") if github_token is not None and github_token_provider is not None: raise ValueError("github_token and github_token_provider are mutually exclusive") + if ask_user_variant not in (None, "legacy", "elicitation"): + raise ValueError('ask_user_variant must be "legacy" or "elicitation"') if not self._client: await self.start() @@ -2552,6 +2561,8 @@ async def create_session( # Enable user input request callback if handler provided if on_user_input_request: payload["requestUserInput"] = True + if ask_user_variant is not None: + payload["askUserVariant"] = ask_user_variant # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) @@ -2970,6 +2981,7 @@ async def resume_session( available_tools: list[str] | ToolSet | None = None, excluded_tools: list[str] | ToolSet | None = None, on_user_input_request: UserInputHandler | None = None, + ask_user_variant: AskUserVariant | None = None, hooks: SessionHooks | None = None, working_directory: str | None = None, additional_directories: list[str] | None = None, @@ -3073,6 +3085,10 @@ async def resume_session( including custom tools registered via ``tools=``. Ignored if ``available_tools`` is set. on_user_input_request: Handler for user input requests. + ask_user_variant: Model-facing shape of the ``ask_user`` tool. + Accepted values are ``"legacy"`` and ``"elicitation"``. The + default is ``"legacy"``. To use ``"elicitation"``, also provide + ``on_elicitation_request`` so the host can answer structured forms. hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. @@ -3226,6 +3242,8 @@ async def resume_session( raise ValueError("on_permission_request must be callable when provided.") if github_token is not None and github_token_provider is not None: raise ValueError("github_token and github_token_provider are mutually exclusive") + if ask_user_variant not in (None, "legacy", "elicitation"): + raise ValueError('ask_user_variant must be "legacy" or "elicitation"') if not self._client: await self.start() @@ -3341,6 +3359,8 @@ async def resume_session( if on_user_input_request: payload["requestUserInput"] = True + if ask_user_variant is not None: + payload["askUserVariant"] = ask_user_variant # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) diff --git a/python/copilot/session.py b/python/copilot/session.py index 78afdde139..3c6d3d54a4 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -504,7 +504,7 @@ class McpAuthContext(TypedDict): class UserInputRequest(TypedDict, total=False): - """Request for user input from the agent (enables ask_user tool)""" + """Legacy question-and-answer request from the ask_user tool.""" question: str choices: list[str] diff --git a/python/test_client.py b/python/test_client.py index a33f0ecd60..bfcee83db2 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -211,6 +211,61 @@ async def test_resume_session_allows_none_permission_handler(self): class TestCreateSessionConfig: + @pytest.mark.asyncio + async def test_ask_user_variant_forwarded_on_create_and_cold_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)) + result = {"sessionId": params["sessionId"], "workspacePath": None} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + + client._client.request = mock_request + await client.create_session( + session_id="ask-user-create", + ask_user_variant="elicitation", + ) + await client.resume_session( + "ask-user-cold-resume", + ask_user_variant="legacy", + ) + await client.create_session(session_id="ask-user-default-create") + await client.resume_session("ask-user-default-cold-resume") + + payloads = {(method, params["sessionId"]): params for method, params in captured} + assert ( + payloads[("session.create", "ask-user-create")]["askUserVariant"] == "elicitation" + ) + assert ( + payloads[("session.resume", "ask-user-cold-resume")]["askUserVariant"] == "legacy" + ) + assert "askUserVariant" not in payloads[("session.create", "ask-user-default-create")] + assert ( + "askUserVariant" not in payloads[("session.resume", "ask-user-default-cold-resume")] + ) + finally: + await client.force_stop() + + @pytest.mark.asyncio + @pytest.mark.parametrize("method", ["create", "resume"]) + async def test_ask_user_variant_rejects_unknown_values(self, method): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + + with pytest.raises(ValueError, match="ask_user_variant"): + if method == "create": + await client.create_session(ask_user_variant="unknown") # type: ignore[arg-type] + else: + await client.resume_session( + "ask-user-cold-resume", + ask_user_variant="unknown", # type: ignore[arg-type] + ) + @pytest.mark.asyncio async def test_additional_directories_forwarded_on_create_and_resume(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/rust/README.md b/rust/README.md index 323d525d37..6114105c25 100644 --- a/rust/README.md +++ b/rust/README.md @@ -274,6 +274,11 @@ let config = SessionConfig { let session = client.create_session(config).await?; ``` +Use `with_ask_user_variant(AskUserVariant::Elicitation)` together with +`with_elicitation_handler(...)` to expose the structured form-based `ask_user` +tool. The default remains `AskUserVariant::Legacy`. Re-supply the option and +handler through `ResumeSessionConfig` on a cold resume. + For rotating per-session GitHub credentials, install a `GitHubTokenProvider` instead of setting `github_token`: @@ -521,6 +526,7 @@ impl ElicitationHandler for MyElicitation { let config = SessionConfig::default() .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_ask_user_variant(AskUserVariant::Elicitation) .with_elicitation_handler(Arc::new(MyElicitation)); ``` diff --git a/rust/src/handler.rs b/rust/src/handler.rs index e036b75a10..61d9b192cb 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -294,10 +294,11 @@ pub trait McpAuthHandler: Send + Sync + 'static { ) -> McpAuthResult; } -/// Handler for `user_input.requested` events from the `ask_user` tool. +/// Handler for `user_input.requested` events from the legacy question-and-answer +/// `ask_user` variant. /// -/// When unset, `requestUserInput: false` goes on the wire and the -/// `ask_user` tool is disabled for the session. +/// When unset, `requestUserInput: false` goes on the wire, so this client +/// cannot handle legacy user-input requests. #[async_trait] pub trait UserInputHandler: Send + Sync + 'static { /// Answer a question on behalf of the user. Return `None` to signal diff --git a/rust/src/types.rs b/rust/src/types.rs index 6e451eb452..726bc48f4c 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1851,6 +1851,18 @@ impl ManagedSettings { } } +/// Selects the model-facing shape of the built-in `ask_user` tool. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +#[non_exhaustive] +pub enum AskUserVariant { + /// Use the legacy user-input request flow. + #[default] + Legacy, + /// Use the elicitation request flow. + Elicitation, +} + /// Configuration for creating a new session via the `session.create` RPC. /// /// All fields are optional — the CLI applies sensible defaults. @@ -1924,6 +1936,11 @@ pub struct SessionConfig { pub streaming: Option, /// Custom system message configuration. pub system_message: Option, + /// Selects the model-facing shape of the built-in `ask_user` tool. + /// + /// When omitted, the runtime uses [`AskUserVariant::Legacy`]. To use + /// [`AskUserVariant::Elicitation`], also install an [`ElicitationHandler`]. + pub ask_user_variant: Option, /// Client-defined tool declarations to expose to the agent. pub tools: Option>, /// Canvas declarations this connection provides to the runtime. @@ -2181,9 +2198,9 @@ pub struct SessionConfig { /// 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. + /// Optional handler for the legacy question-and-answer `ask_user` variant. + /// When `None`, `requestUserInput: false` goes on the wire, so this client + /// cannot handle legacy user-input requests. pub user_input_handler: Option>, /// Optional exit-plan-mode handler. When `None`, /// `requestExitPlanMode: false` goes on the wire. @@ -2238,6 +2255,7 @@ impl std::fmt::Debug for SessionConfig { .field("context_tier", &self.context_tier) .field("streaming", &self.streaming) .field("system_message", &self.system_message) + .field("ask_user_variant", &self.ask_user_variant) .field("tools", &self.tools) .field("canvases", &self.canvases) .field( @@ -2378,6 +2396,7 @@ impl Default for SessionConfig { context_tier: None, streaming: None, system_message: None, + ask_user_variant: None, tools: None, canvases: None, canvas_handler: None, @@ -2545,6 +2564,7 @@ impl SessionConfig { context_tier: self.context_tier, streaming: self.streaming, system_message: self.system_message, + ask_user_variant: self.ask_user_variant, tools: self.tools, canvases: wire_canvases, request_canvas_renderer: self.request_canvas_renderer, @@ -2656,13 +2676,19 @@ impl SessionConfig { self } - /// Install a [`UserInputHandler`]. Required for the `ask_user` tool - /// to be enabled. + /// Install a [`UserInputHandler`] for the legacy question-and-answer + /// `ask_user` variant. pub fn with_user_input_handler(mut self, handler: Arc) -> Self { self.user_input_handler = Some(handler); self } + /// Select the model-facing shape of the built-in `ask_user` tool. + pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self { + self.ask_user_variant = Some(variant); + self + } + /// Install an [`ExitPlanModeHandler`]. pub fn with_exit_plan_mode_handler(mut self, handler: Arc) -> Self { self.exit_plan_mode_handler = Some(handler); @@ -3304,6 +3330,11 @@ pub struct ResumeSessionConfig { /// Re-supply the system message so the agent retains workspace context /// across CLI process restarts. pub system_message: Option, + /// Selects the model-facing shape of the built-in `ask_user` tool on a cold resume. + /// + /// When omitted, the runtime uses [`AskUserVariant::Legacy`]. To use + /// [`AskUserVariant::Elicitation`], also install an [`ElicitationHandler`]. + pub ask_user_variant: Option, /// Client-defined tool declarations to re-supply on resume. pub tools: Option>, /// Canvas declarations this connection provides to the runtime. @@ -3547,6 +3578,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("context_tier", &self.context_tier) .field("streaming", &self.streaming) .field("system_message", &self.system_message) + .field("ask_user_variant", &self.ask_user_variant) .field("tools", &self.tools) .field("canvases", &self.canvases) .field( @@ -3730,6 +3762,7 @@ impl ResumeSessionConfig { context_tier: self.context_tier, streaming: self.streaming, system_message: self.system_message, + ask_user_variant: self.ask_user_variant, tools: self.tools, canvases: wire_canvases, open_canvases: self.open_canvases, @@ -3836,6 +3869,7 @@ impl ResumeSessionConfig { context_tier: None, streaming: None, system_message: None, + ask_user_variant: None, tools: None, canvases: None, canvas_handler: None, @@ -3939,6 +3973,12 @@ impl ResumeSessionConfig { self } + /// Select the model-facing shape of the built-in `ask_user` tool on resume. + pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self { + self.ask_user_variant = Some(variant); + self + } + /// Install an [`ExitPlanModeHandler`] for the resumed session. pub fn with_exit_plan_mode_handler(mut self, handler: Arc) -> Self { self.exit_plan_mode_handler = Some(handler); @@ -6239,6 +6279,8 @@ mod tests { assert!(!wire.request_auto_mode_switch); assert!(!wire.hooks); assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); } #[test] @@ -6255,6 +6297,8 @@ mod tests { assert!(!wire.request_auto_mode_switch); assert!(!wire.hooks); assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); } #[test] diff --git a/rust/src/wire.rs b/rust/src/wire.rs index f7de33839c..ebc050477e 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -24,11 +24,11 @@ use crate::generated::api_types::{ }; use crate::generated::session_events::ReasoningSummary; use crate::types::{ - CanvasProviderIdentity, CapiSessionOptions, CloudSessionOptions, CustomAgentConfig, - DefaultAgentConfig, ExtensionInfo, GitHubMcpToolConfig, InfiniteSessionConfig, - LargeToolOutputConfig, McpServerConfig, MemoryConfiguration, NamedProviderConfig, - ProviderConfig, ProviderModelConfig, SessionId, SessionLimitsConfig, SystemMessageConfig, Tool, - ToolSearchConfig, + AskUserVariant, CanvasProviderIdentity, CapiSessionOptions, CloudSessionOptions, + CustomAgentConfig, DefaultAgentConfig, ExtensionInfo, GitHubMcpToolConfig, + InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, MemoryConfiguration, + NamedProviderConfig, ProviderConfig, ProviderModelConfig, SessionId, SessionLimitsConfig, + SystemMessageConfig, Tool, ToolSearchConfig, }; /// Wire representation of a slash command (name + description only). The @@ -64,6 +64,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub system_message: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub ask_user_variant: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub canvases: Option>, @@ -218,6 +220,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub system_message: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub ask_user_variant: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub canvases: Option>, diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index a51d61910d..fb16a0f674 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -24,8 +24,8 @@ use github_copilot_sdk::session_events::{ SessionManagedSettingsResolvedData, }; use github_copilot_sdk::types::{ - CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, CommandContext, - CommandDefinition, CommandHandler, DeliveryMode, DisableBypassPermissionsModes, + AskUserVariant, CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, + CommandContext, CommandDefinition, CommandHandler, DeliveryMode, DisableBypassPermissionsModes, ElicitationRequest, ElicitationResult, ExitPlanModeData, ExtensionInfo, ManagedSettings, ManagedSettingsPermissions, MessageOptions, PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, PermissionDecisionSurface, RequestId, @@ -918,6 +918,60 @@ async fn create_session_sends_new_session_options() { timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); } +#[tokio::test] +async fn create_session_forwards_ask_user_variant() { + 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_ask_user_variant(AskUserVariant::Elicitation), + ) + .await + .unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "session.create"); + assert_eq!(request["params"]["askUserVariant"], "elicitation"); + + let session_id = requested_session_id(&request).to_string(); + server_respond_create(&mut server_write, &request, &session_id).await; + timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn cold_resume_session_forwards_ask_user_variant() { + 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("ask-user-variant")) + .with_ask_user_variant(AskUserVariant::Legacy), + ) + .await + .unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "session.resume"); + assert_eq!(request["params"]["sessionId"], "ask-user-variant"); + assert_eq!(request["params"]["askUserVariant"], "legacy"); + + server_respond_create(&mut server_write, &request, "ask-user-variant").await; + respond_to_reload(&mut server_read, &mut server_write).await; + timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); +} + #[tokio::test] async fn resume_session_sends_new_session_options() { use github_copilot_sdk::types::ResumeSessionConfig; From 128927eb6590394a1d8de5a646da07873e1523ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 1 Sep 2026 08:30:49 +0000 Subject: [PATCH 08/60] docs: update version references to 1.0.13-preview.3 --- java/README.md | 8 ++++---- java/sdk/jbang-example.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java/README.md b/java/README.md index e64964255b..fb4aeb9d25 100644 --- a/java/README.md +++ b/java/README.md @@ -32,14 +32,14 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. com.github copilot-sdk-java - 1.0.13-preview.2 + 1.0.13-preview.3 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.13-preview.2' +implementation 'com.github:copilot-sdk-java:1.0.13-preview.3' ``` #### Snapshot Builds @@ -58,7 +58,7 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.14-preview.2-SNAPSHOT + 1.0.14-preview.3-SNAPSHOT ``` @@ -67,7 +67,7 @@ Snapshot builds of the next development version are published to Maven Central S Replace `${copilot.sdk.version}` with the latest release from Maven Central. ```groovy -implementation 'com.github:copilot-sdk-java:1.0.14-preview.2-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.14-preview.3-SNAPSHOT' ``` ## In-process mode (experimental) diff --git a/java/sdk/jbang-example.java b/java/sdk/jbang-example.java index 6cb04215ac..afff013803 100644 --- a/java/sdk/jbang-example.java +++ b/java/sdk/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.13-preview.2 +//DEPS com.github:copilot-sdk-java:1.0.13-preview.3 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; From 07ae7a18c078514c32d22a9cc37a82b4d083c335 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 1 Sep 2026 08:31:20 +0000 Subject: [PATCH 09/60] [maven-release-plugin] prepare release java/v1.0.13-preview.3 --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index d138a0e56b..7466113a01 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.2-SNAPSHOT + 1.0.13-preview.3 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.3 diff --git a/java/pom.xml b/java/pom.xml index 427bc2320c..f7e5cbf53e 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.2-SNAPSHOT + 1.0.13-preview.3 pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.3 diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index 811a05db67..b9ec366282 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.2-SNAPSHOT + 1.0.13-preview.3 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.3 From 6adcf00d433614d550563835b688012d5b9c5782 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 1 Sep 2026 08:31:24 +0000 Subject: [PATCH 10/60] [maven-release-plugin] prepare for next development iteration --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 7466113a01..a0272acdc4 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.3 + 1.0.14-preview.3-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.3 + HEAD diff --git a/java/pom.xml b/java/pom.xml index f7e5cbf53e..bd317cd197 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.3 + 1.0.14-preview.3-SNAPSHOT pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.3 + HEAD diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index b9ec366282..46ae52e487 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.3 + 1.0.14-preview.3-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.3 + HEAD From 538b2dce30755ca53f60b5ad42d11a98addf8edb Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 1 Sep 2026 08:25:14 +0000 Subject: [PATCH 11/60] Launch managed SDK servers through the Rust runtime wrapper (#2395) * Integrate out-of-process Rust runtime wrapper Stage and launch copilot-runtime beside runtime.node across all SDKs, with an opt-in local runtime-worktree override until published packages include the wrapper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Validate runtime wrapper across SDK harnesses Route out-of-process E2E harnesses through the local runtime override while preserving the residual CLI for in-process execution. Keep Java bundled residual injection intact and surface Python fast-exit diagnostics reliably. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Use residual CLI for Python FFI test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Honor per-client runtime environment in .NET Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Handle Rust session requests during creation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Skip Go telemetry callback test in-process Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Add legacy CLI launch escape hatch Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Remove residual Node runtime compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Remove COPILOT_RUNTIME_PATH override Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * fix(rust): materialize runtime launch contract Ensure managed wrapper launches materialize and hand off the compatible host, and expose the same launch descriptor for health checks and intermediate launchers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Remove residual runtime host contract Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2455380f-4747-4900-89ca-93b30399de03 * fix(rust): materialize sibling CLI host Extract the bundled CLI beside copilot-runtime and runtime.node so managed and intermediate launches retain compatibility through the wrapper's sibling fallback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * fix(rust): create runtime install directory Ensure a clean bundled runtime cache can publish runtime.node, the wrapper, and sibling CLI before any artifact path exists. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Complete managed runtime bundle materialization Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Remove managed SEA staging for hostless runtime Keep the root CLI artifact bundled for direct and in-process use, while managed out-of-process caches materialize only copilot-runtime and runtime.node. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Stage auxiliary runtime assets from npm packages Preserve unknown package assets by default while filtering known CLI-only content in each SDK's existing staging path. Keep wrapper companions adjacent, migrate caches safely, and retain executable metadata for external runtime tools. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Exclude runtime package documentation from staging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Finalize runtime wrapper integration after rebase Preserve the explicit .NET single-binary override, refresh stale Rust runtime bundles safely, and document the Python npm registry override. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Use executable cache for Node runtime wrapper Stage the managed wrapper under the platform user cache instead of the temporary directory so noexec temp mounts do not prevent startup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Temporarily skip extension-host E2E coverage Document PR #2395 and the Rust-only out-of-process transition on Node extension-authored factory coverage, the real-host extension environment test, and .NET extension lifecycle tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Address runtime wrapper review feedback Match the successful grep completion to the grep tool invocation and document Java's bundled runtime-wrapper default. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Stop resolving SEA for in-process hosting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Keep default runtime bundles SEA-free Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix cross-platform runtime integration tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix runtime CI harness portability Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Relax closed-stream startup assertion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix failed-start runtime test handling Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Format Python runtime test setup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Clean up failed Node runtime startup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix Java token provider sample Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Fix runtime integration CI checks Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Make Java relative path test drive-safe Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Use generated logging for permission failures Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Suppress Node pipe writes after runtime exit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Preserve Node startup transport errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * Add Rust extension launch provider Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Clarify extension launch environment Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: efc29e35-611b-46d8-9b8b-e1912d2eccf4 * test(rust): cover runtime extension lifecycle Add a real-wrapper regression that exercises typed provider dispatch, extension registration, tool invocation, disable/re-enable, environment ownership, and process teardown. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * fix(java): honor runtime path environment override Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 * test(node): skip host-dependent ask user metadata Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(node): preserve startup stderr after pipe errors Wait for the child close event before selecting startup diagnostics so early EPIPE failures do not hide the runtime's actionable stderr output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * refactor(node): separate exit and diagnostic waits Keep startup RPCs fail-fast on process exit while using child close only to collect fully drained stderr after an early pipe error. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test(node): preserve ask user metadata coverage Run the Node-hosted metadata assertion through the harness's explicit legacy CLI path instead of disabling it for managed hostless connections. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * refactor(node): simplify startup failure handling Keep startup teardown fail-fast and verify startup failures without waiting separately for diagnostic stream closure. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * style(node): format startup failure test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * test(python): set plan mode before handler test Await the explicit mode transition before sending the replayed prompt so exit_plan_mode is deterministically available to the model. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * test: preserve live in-process runtime state Keep isolated runtime home files until the shared in-process client shuts down so later tests never operate on POSIX-unlinked state. Apply the same lifecycle rule to the Node and Python E2E harnesses. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * test: stabilize transient runtime checks Observe Rust processing state concurrently with message dispatch so fast in-process turns cannot complete before polling starts. Exercise rewind through an edit of a pre-existing file across SDKs, avoiding the runtime's unreliable new-file capture path while preserving file restoration coverage on every platform. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * Stabilize Rust processing state E2E Hold a replayed custom tool invocation open while asserting the transient processing state, then release it and verify completion. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * Stabilize Windows .NET E2E coverage Use explicit .NET Framework handling for nondeterministic stdio startup diagnostics and complete a warm-up turn before exercising rewind file tracking across SDKs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * test: stabilize rewind and Rust resume E2E Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 * fix: stabilize runtime disconnect races Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Stephen Toub Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015 Copilot-Session: 2455380f-4747-4900-89ca-93b30399de03 Copilot-Session: efc29e35-611b-46d8-9b8b-e1912d2eccf4 Copilot-Session: b90d19df-b3fa-4539-b921-8b301028da11 --- dotnet/README.md | 5 + dotnet/src/Client.cs | 160 ++++-- dotnet/src/FfiRuntimeHost.cs | 84 ++- dotnet/src/Session.cs | 5 +- dotnet/src/build/GitHub.Copilot.SDK.targets | 57 ++ dotnet/test/E2E/BuiltinToolsE2ETests.cs | 17 - dotnet/test/E2E/ClientE2ETests.cs | 18 +- dotnet/test/E2E/PendingWorkResumeE2ETests.cs | 23 + dotnet/test/E2E/RewindE2ETests.cs | 34 +- dotnet/test/Unit/MSBuildTargetsTests.cs | 77 +++ dotnet/test/Unit/RuntimeWrapperTests.cs | 134 +++++ go/README.md | 7 +- go/client.go | 42 +- go/client_test.go | 29 + go/cmd/bundler/main.go | 393 +++++++++++--- go/cmd/bundler/main_test.go | 126 ++++- go/inprocess_disabled.go | 2 +- go/inprocess_enabled.go | 4 +- go/internal/e2e/inprocess_ffi_e2e_test.go | 4 +- .../e2e/pending_work_resume_e2e_test.go | 25 + go/internal/e2e/rewind_e2e_test.go | 66 ++- go/internal/embeddedcli/embeddedcli.go | 292 +++++++++- go/internal/embeddedcli/embeddedcli_test.go | 136 +++++ go/internal/ffihost/ffihost.go | 45 +- go/internal/ffihost/ffihost_test.go | 45 +- go/internal/ffihost/resolve.go | 10 +- java/README.md | 16 +- java/copilot-native/pom.xml | 15 +- .../create-native-classifier-test-fixture.mjs | 8 +- java/copilot-native/scripts/fetch-native.mjs | 160 ++++-- .../scripts/fetch-native.test.mjs | 128 ++++- .../scripts/validate-native-artifact.mjs | 8 +- .../scripts/validate-native-artifact.test.mjs | 10 +- .../com/github/copilot/CliServerManager.java | 124 +++-- .../com/github/copilot/CopilotClient.java | 13 +- .../github/copilot/ffi/FfiRuntimeHost.java | 19 +- .../copilot/ffi/NativeRuntimeLoader.java | 151 +++++- .../copilot/rpc/CopilotClientOptions.java | 9 +- .../github/copilot/CliServerManagerTest.java | 45 ++ .../copilot/e2e/InProcessTransportIT.java | 13 +- .../copilot/e2e/OutOfProcessTransportIT.java | 38 ++ .../java/com/github/copilot/e2e/RewindIT.java | 42 +- .../copilot/ffi/FfiRuntimeHostTest.java | 41 ++ .../copilot/ffi/NativeRuntimeLoaderTest.java | 136 ++++- nodejs/README.md | 1 + nodejs/src/client.ts | 121 +++-- nodejs/src/ffiRuntimeHost.ts | 65 +-- nodejs/src/runtimeArtifacts.ts | 183 +++++++ nodejs/test/client.test.ts | 22 + nodejs/test/e2e/builtin_tools.e2e.test.ts | 14 + nodejs/test/e2e/client.e2e.test.ts | 18 +- .../test/e2e/extension_env_access.e2e.test.ts | 87 ++- nodejs/test/e2e/factory.e2e.test.ts | 508 ++++++++---------- nodejs/test/e2e/harness/sdkTestContext.ts | 32 +- nodejs/test/e2e/inprocess_ffi.e2e.test.ts | 5 +- .../test/e2e/pending_work_resume.e2e.test.ts | 42 +- nodejs/test/e2e/rewind.e2e.test.ts | 117 ++-- nodejs/test/e2e/ui_elicitation.e2e.test.ts | 40 +- nodejs/test/runtimeArtifacts.test.ts | 109 ++++ python/README.md | 24 +- python/copilot/_cli_download.py | 165 +++++- python/copilot/_ffi_runtime_host.py | 56 +- python/copilot/client.py | 82 +-- python/e2e/conftest.py | 11 + python/e2e/test_inprocess_ffi_e2e.py | 12 +- python/e2e/test_mode_handlers_e2e.py | 8 +- python/e2e/test_rewind_e2e.py | 31 +- python/e2e/testharness/context.py | 13 +- python/test_cli_download.py | 114 ++++ python/test_client.py | 21 + rust/Cargo.toml | 15 + rust/README.md | 138 +++-- rust/build.rs | 5 - rust/build/in_process.rs | 359 +++++++++---- rust/src/embeddedcli.rs | 329 ++++++++++-- rust/src/extension_launch_provider.rs | 158 ++++++ rust/src/ffi.rs | 128 ++--- rust/src/lib.rs | 176 +++++- rust/src/resolve.rs | 157 +++++- rust/src/router.rs | 7 + rust/src/startup_timings.rs | 2 +- rust/tests/cli_resolution_test.rs | 145 +++-- rust/tests/e2e/rewind.rs | 47 +- rust/tests/e2e/rpc_session_state.rs | 63 ++- rust/tests/e2e/session.rs | 5 +- rust/tests/e2e/support.rs | 11 +- .../extension_launch_provider_runtime_test.rs | 321 +++++++++++ rust/tests/extension_launch_provider_test.rs | 493 +++++++++++++++++ rust/tests/fixtures/extension_fixture.rs | 127 +++++ ...restore_tracked_file_and_conversation.yaml | 26 +- ...eport_processing_and_context_metadata.yaml | 12 +- 91 files changed, 5941 insertions(+), 1500 deletions(-) create mode 100644 dotnet/test/Unit/RuntimeWrapperTests.cs create mode 100644 java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java create mode 100644 nodejs/src/runtimeArtifacts.ts create mode 100644 nodejs/test/runtimeArtifacts.test.ts create mode 100644 rust/src/extension_launch_provider.rs create mode 100644 rust/tests/extension_launch_provider_runtime_test.rs create mode 100644 rust/tests/extension_launch_provider_test.rs create mode 100644 rust/tests/fixtures/extension_fixture.rs diff --git a/dotnet/README.md b/dotnet/README.md index a6a419644f..23a78030b4 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -101,6 +101,11 @@ new CopilotClient(CopilotClientOptions? options = null) - `RuntimeConnection.ForTcp(port = 0, connectionToken?, path?, args?)` — spawns the runtime as a child process listening on a TCP port. `port = 0` auto-allocates; if a non-zero port is already in use, startup fails (no fallback). Use `CopilotClient.RuntimePort` after `StartAsync` to read the assigned port. `connectionToken` is required if other clients will connect via `RuntimeConnection.ForUri(...)`. - `RuntimeConnection.ForUri(url, connectionToken?)` — connects to an already-running runtime at `url` (e.g., `"localhost:8080"`). Does not spawn a process. +Managed stdio and TCP connections use the bundled `copilot-runtime[.exe]` and +adjacent `runtime.node` by default. An explicit connection path or +`COPILOT_CLI_PATH` overrides the bundled runtime. +Managed launch fails if the bundled wrapper pair is unavailable. + #### Methods ##### `StartAsync(): Task` diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 157e519811..092112cebc 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -55,6 +55,7 @@ namespace GitHub.Copilot; /// public sealed partial class CopilotClient : IDisposable, IAsyncDisposable { + private const string ExplicitBundledCliMarker = ".copilot-explicit-cli"; ///

/// Minimum protocol version this SDK can communicate with. /// @@ -416,9 +417,19 @@ async Task StartCoreAsync(CancellationToken ct) ffiArgs.Add("--remote"); } + var explicitCliPath = System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); + if (string.IsNullOrEmpty(explicitCliPath)) + { + explicitCliPath = null; + } + var ffiRuntimePath = explicitCliPath is null + ? GetBundledNativePath(FfiRuntimeHost.GetRuntimeLibraryFileName(), out var searchedRuntime) + ?? throw new InvalidOperationException( + $"In-process FFI runtime library not found at '{searchedRuntime}'.") + : ResolveRuntimePathForExplicitCli(explicitCliPath); var ffiHost = FfiRuntimeHost.Create( - ResolveCliPathForFfi(), - GetNapiPrebuildsFolderOrThrow(), + ffiRuntimePath, + explicitCliPath, ffiEnvironment, ffiArgs, _logger); @@ -493,6 +504,20 @@ await InvokeRpcAsync( await CleanupCliProcessAsync(cliProcess, stderrPump, errors: null, _logger); } + if (ex is IOException + && cliProcess is not null + && stderrPump is not null + && !ex.Message.Contains("stderr:", StringComparison.OrdinalIgnoreCase)) + { + var stderrOutput = GetStderrOutput(stderrPump.Buffer); + if (!string.IsNullOrEmpty(stderrOutput)) + { + throw new IOException( + FormatCliExitedMessage("CLI process exited unexpectedly.", stderrOutput), + ex); + } + } + throw; } } @@ -673,7 +698,7 @@ or IOException private static async Task CleanupCliProcessAsync(Process childProcess, ProcessStderrPump? stderrPump, List? errors, ILogger? logger) { - stderrPump?.Cancel(); + var processExited = false; try { @@ -706,12 +731,19 @@ private static async Task CleanupCliProcessAsync(Process childProcess, ProcessSt AddCleanupError(errors, ex, logger); } } + + processExited = childProcess.HasExited; } catch (Exception ex) { AddCleanupError(errors, ex, logger); } + if (!processExited) + { + stderrPump?.Cancel(); + } + if (stderrPump is not null) { var stderrPumpWaitTimestamp = Stopwatch.GetTimestamp(); @@ -721,6 +753,7 @@ private static async Task CleanupCliProcessAsync(Process childProcess, ProcessSt } catch (TimeoutException ex) { + stderrPump.Cancel(); if (logger is not null) { LoggingHelpers.LogTiming(logger, LogLevel.Debug, ex, @@ -1960,13 +1993,15 @@ private static string FormatCliExitedMessage(string message, string stderrOutput private static IOException CreateCliExitedException(string message, StringBuilder stderrBuffer) { - string stderrOutput; + return new IOException(FormatCliExitedMessage(message, GetStderrOutput(stderrBuffer))); + } + + private static string GetStderrOutput(StringBuilder stderrBuffer) + { lock (stderrBuffer) { - stderrOutput = stderrBuffer.ToString().Trim(); + return stderrBuffer.ToString().Trim(); } - - return new IOException(FormatCliExitedMessage(message, stderrOutput)); } private Task EnsureConnectedAsync(CancellationToken cancellationToken) @@ -2217,17 +2252,19 @@ private static void ApplyTelemetryEnvironment(IDictionary envir var tcpConnection = _connection as TcpRuntimeConnection; var useStdio = _connection is StdioRuntimeConnection; - // Use explicit path, COPILOT_CLI_PATH env var (from the connection's - // Environment, options.Environment, or process env), or bundled runtime - no PATH fallback - var envCliPath = - (childProcessConnection.Environment is not null && childProcessConnection.Environment.TryGetValue("COPILOT_CLI_PATH", out var connEnvValue) ? connEnvValue : null) - ?? (options.Environment is not null && options.Environment.TryGetValue("COPILOT_CLI_PATH", out var envValue) ? envValue : null) - ?? System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); - var cliPath = childProcessConnection.Path - ?? envCliPath - ?? GetBundledCliPath(out var searchedPath) - ?? throw new InvalidOperationException($"Copilot runtime not found at '{searchedPath}'. Ensure the SDK NuGet package was restored correctly or provide an explicit RuntimeConnection.ForStdio(path: ...) / RuntimeConnection.ForTcp(path: ...)."); - var cliPathSource = childProcessConnection.Path is not null ? "Options" : envCliPath is not null ? "Environment" : "Bundled"; + // Explicit CLI paths preserve the legacy launch contract. Otherwise use + // the bundled native runtime pair. + var configuredEnvironment = childProcessConnection.Environment ?? options.Environment; + var envCliPath = configuredEnvironment is not null + ? configuredEnvironment.TryGetValue("COPILOT_CLI_PATH", out var configuredCliPath) ? configuredCliPath : null + : System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); + var launch = childProcessConnection.Path is not null + ? new RuntimeLaunch(childProcessConnection.Path, "Options") + : envCliPath is not null + ? new RuntimeLaunch(envCliPath, "Environment") + : GetBundledRuntimeLaunch(); + var cliPath = launch.Executable; + var cliPathSource = launch.Source; var args = new List(); if (childProcessConnection.Args != null) @@ -2409,7 +2446,11 @@ private static void ApplyTelemetryEnvironment(IDictionary envir private static string? GetBundledCliPath(out string searchedPath) { - var binaryName = OperatingSystem.IsWindows() ? "copilot.exe" : "copilot"; + return GetBundledNativePath(OperatingSystem.IsWindows() ? "copilot.exe" : "copilot", out searchedPath); + } + + private static string? GetBundledNativePath(string binaryName, out string searchedPath) + { // Always use portable RID (e.g., linux-x64) to match the build-time placement, // since distro-specific RIDs (e.g., ubuntu.24.04-x64) are normalized at build time. var rid = GetPortableRid() @@ -2418,6 +2459,57 @@ private static void ApplyTelemetryEnvironment(IDictionary envir return File.Exists(searchedPath) ? searchedPath : null; } + private static RuntimeLaunch GetBundledRuntimeLaunch() + { + _ = GetBundledNativePath( + OperatingSystem.IsWindows() ? "copilot-runtime.exe" : "copilot-runtime", + out var searchedWrapper); + var directory = Path.GetDirectoryName(searchedWrapper)!; + var runtimeNode = Path.Combine(directory, "runtime.node"); + var explicitCliMarker = Path.Combine(directory, ExplicitBundledCliMarker); + if (!File.Exists(searchedWrapper) + && !File.Exists(runtimeNode) + && File.Exists(explicitCliMarker) + && GetBundledCliPath(out _) is { } explicitCli) + { + return new RuntimeLaunch(explicitCli, "Bundled explicit CLI"); + } + return ValidateRuntimePair(searchedWrapper, "Bundled runtime"); + } + + private static RuntimeLaunch ValidateRuntimePair(string wrapper, string source) + { + var runtimeNode = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(wrapper))!, "runtime.node"); + if (!File.Exists(wrapper)) + { + throw new InvalidOperationException($"Copilot runtime wrapper not found at '{wrapper}'."); + } + if (!File.Exists(runtimeNode)) + { + throw new InvalidOperationException( + $"Copilot runtime wrapper at '{wrapper}' is missing its adjacent runtime.node at '{runtimeNode}'."); + } + if (new FileInfo(wrapper).Length == 0 || new FileInfo(runtimeNode).Length == 0) + { + throw new InvalidOperationException("Copilot runtime wrapper and adjacent runtime.node must both be non-empty."); + } +#if NET8_0_OR_GREATER + if (!OperatingSystem.IsWindows()) + { + var mode = File.GetUnixFileMode(wrapper); + const UnixFileMode executeBits = + UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; + if ((mode & executeBits) == 0) + { + File.SetUnixFileMode(wrapper, mode | executeBits); + } + } +#endif + return new RuntimeLaunch(wrapper, source); + } + + private sealed record RuntimeLaunch(string Executable, string Source); + private static string? GetPortableRid() { string os; @@ -2441,26 +2533,22 @@ private static void ApplyTelemetryEnvironment(IDictionary envir return arch != null ? $"{os}-{arch}" : null; } - private string ResolveCliPathForFfi() + private static string ResolveRuntimePathForExplicitCli(string cliPath) { - var envCliPath = _options.Environment is not null && _options.Environment.TryGetValue("COPILOT_CLI_PATH", out var envValue) - ? envValue - : System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); - if (!string.IsNullOrEmpty(envCliPath)) + var fullEntrypoint = Path.GetFullPath(cliPath); + var directory = Path.GetDirectoryName(fullEntrypoint) + ?? throw new InvalidOperationException($"Could not determine directory for '{cliPath}'."); + var flatLibraryPath = Path.Combine(directory, FfiRuntimeHost.GetRuntimeLibraryFileName()); + if (File.Exists(flatLibraryPath)) { - return envCliPath; + return flatLibraryPath; } - - // Fall back to the bundled single-file CLI the same way stdio discovers it. - // It embeds its own Node and is spawned directly as `copilot --embedded-host`, - // with the sibling cdylib loaded in-process (FfiRuntimeHost.Create prefers the - // flat `libcopilot_runtime.so`/`copilot_runtime.dll` next to the CLI, falling - // back to the dev `prebuilds//runtime.node` layout). - var bundled = GetBundledCliPath(out var searchedPath); - return bundled - ?? throw new InvalidOperationException( - "In-process FFI hosting requires the Copilot CLI. Set the COPILOT_CLI_PATH " - + $"environment variable, or ensure the bundled CLI is present (looked in '{searchedPath}')."); + var prebuildsLibraryPath = Path.Combine( + directory, "prebuilds", GetNapiPrebuildsFolderOrThrow(), "runtime.node"); + return File.Exists(prebuildsLibraryPath) + ? prebuildsLibraryPath + : throw new InvalidOperationException( + $"FFI runtime library not found. Looked for '{flatLibraryPath}' and '{prebuildsLibraryPath}'."); } /// diff --git a/dotnet/src/FfiRuntimeHost.cs b/dotnet/src/FfiRuntimeHost.cs index a838b9fd17..cc3bccae92 100644 --- a/dotnet/src/FfiRuntimeHost.cs +++ b/dotnet/src/FfiRuntimeHost.cs @@ -17,11 +17,9 @@ namespace GitHub.Copilot; /// and communicating over stdio/TCP. /// /// -/// The Rust host_start export spawns the residual TypeScript worker itself — -/// typically the packaged single-file CLI (copilot --embedded-host, which embeds -/// its own Node) or, for dev, node dist-cli/index.js --embedded-host — so the .NET -/// host never launches Node directly. JSON-RPC frames are pumped across the ABI: writes go -/// to connection_write; inbound frames arrive on a native callback that feeds +/// The Rust host_start export constructs the server synchronously in this +/// process. JSON-RPC frames are pumped across the ABI: writes go to +/// connection_write; inbound frames arrive on a native callback that feeds /// . /// /// The native interop layer has two implementations selected by target framework. On @@ -41,7 +39,7 @@ internal sealed partial class FfiRuntimeHost : IDisposable private const string LibraryName = "copilot_runtime"; private readonly ILogger _logger; - private readonly string _cliEntrypoint; + private readonly string? _cliEntrypoint; private readonly string _libraryPath; private readonly IReadOnlyDictionary? _environment; private readonly IReadOnlyList _args; @@ -53,7 +51,7 @@ internal sealed partial class FfiRuntimeHost : IDisposable private uint _connectionId; private bool _disposed; - private FfiRuntimeHost(string libraryPath, string cliEntrypoint, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) + private FfiRuntimeHost(string libraryPath, string? cliEntrypoint, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) { _libraryPath = libraryPath; _cliEntrypoint = cliEntrypoint; @@ -70,35 +68,22 @@ private FfiRuntimeHost(string libraryPath, string cliEntrypoint, IReadOnlyDictio ?? throw new InvalidOperationException("FfiRuntimeHost has not been started."); /// - /// Loads the cdylib next to the given CLI entrypoint and prepares the FFI host. - /// The entrypoint is either the packaged single-file CLI binary (e.g. - /// runtimes/<rid>/native/copilot) or, for dev, a .js file (e.g. - /// dist-cli/index.js) launched via node. The cdylib is resolved - /// relative to the entrypoint directory, preferring the flat, natural - /// shared-library name the .NET build emits (e.g. libcopilot_runtime.so) - /// and falling back to the dev tarball layout - /// prebuilds/<prebuildsFolder>/runtime.node, where - /// is the napi-rs - /// <node-platform>-<arch> folder name (e.g. win32-x64). + /// Loads the runtime cdylib and prepares the FFI host. /// - public static FfiRuntimeHost Create(string cliEntrypoint, string prebuildsFolder, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) + public static FfiRuntimeHost Create(string libraryPath, string? cliEntrypoint, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) { - var fullEntrypoint = Path.GetFullPath(cliEntrypoint); - var distDir = Path.GetDirectoryName(fullEntrypoint) - ?? throw new InvalidOperationException($"Could not determine directory for '{cliEntrypoint}'."); - - // Bundled .NET layout: flat, natural shared-library name next to the CLI. - var flatLibraryPath = Path.Combine(distDir, GetRuntimeLibraryFileName()); - // Dev/tarball layout: dist-cli/prebuilds/-/runtime.node. - var prebuildsLibraryPath = Path.Combine(distDir, "prebuilds", prebuildsFolder, "runtime.node"); - - var libraryPath = File.Exists(flatLibraryPath) ? flatLibraryPath - : File.Exists(prebuildsLibraryPath) ? prebuildsLibraryPath - : throw new InvalidOperationException( - $"FFI runtime library not found. Looked for '{flatLibraryPath}' and '{prebuildsLibraryPath}'."); - - PrepareNativeLibrary(libraryPath); - return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args, logger); + var fullLibraryPath = Path.GetFullPath(libraryPath); + if (!File.Exists(fullLibraryPath)) + { + throw new InvalidOperationException($"FFI runtime library not found at '{fullLibraryPath}'."); + } + PrepareNativeLibrary(fullLibraryPath); + return new FfiRuntimeHost( + fullLibraryPath, + cliEntrypoint is null ? null : Path.GetFullPath(cliEntrypoint), + environment, + args, + logger); } /// @@ -106,7 +91,7 @@ public static FfiRuntimeHost Create(string cliEntrypoint, string prebuildsFolder /// emitted by the .NET build (the .node file renamed to what the Rust cdylib /// would be called on this OS). /// - private static string GetRuntimeLibraryFileName() + internal static string GetRuntimeLibraryFileName() { if (OperatingSystem.IsWindows()) return "copilot_runtime.dll"; if (OperatingSystem.IsMacOS()) return "libcopilot_runtime.dylib"; @@ -114,14 +99,11 @@ private static string GetRuntimeLibraryFileName() } /// - /// Starts the in-process runtime: spawns the CLI worker via the Rust host, - /// waits for readiness, and opens the FFI JSON-RPC connection. + /// Starts the in-process Rust runtime and opens the FFI JSON-RPC connection. /// public async Task StartAsync(CancellationToken cancellationToken) { - // host_start blocks until the worker connects back and signals readiness - // (up to ~30s), and connection_open must run outside any async runtime, so - // perform the blocking FFI handshake on a background thread. + // Keep synchronous native startup off the caller's async context. await Task.Run(() => { var argvJson = BuildArgvJson(_cliEntrypoint, _args); @@ -131,7 +113,7 @@ await Task.Run(() => if (_serverId == 0) { throw new InvalidOperationException( - $"copilot_runtime_host_start failed (library '{_libraryPath}', entrypoint '{_cliEntrypoint}')."); + $"copilot_runtime_host_start failed (library '{_libraryPath}')."); } _connectionId = NativeOpenConnection(_serverId); @@ -154,24 +136,22 @@ await Task.Run(() => } } - private static byte[] BuildArgvJson(string cliEntrypoint, IReadOnlyList args) + private static byte[] BuildArgvJson(string? cliEntrypoint, IReadOnlyList args) { - // A .js entrypoint (dev / dist-cli) is launched via node; the packaged - // single-file CLI binary embeds its own Node and is invoked directly. - var isJsFile = cliEntrypoint.EndsWith(".js", StringComparison.OrdinalIgnoreCase); using var stream = new MemoryStream(); using (var writer = new Utf8JsonWriter(stream)) { writer.WriteStartArray(); - if (isJsFile) + if (cliEntrypoint is not null) { - writer.WriteStringValue("node"); + if (cliEntrypoint.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) + { + writer.WriteStringValue("node"); + } + writer.WriteStringValue(cliEntrypoint); + writer.WriteStringValue("--embedded-host"); + writer.WriteStringValue("--no-auto-update"); } - writer.WriteStringValue(cliEntrypoint); - writer.WriteStringValue("--embedded-host"); - // Pin the worker to the bundled pkg matching the loaded cdylib, instead of - // drifting to a newer version under the user's ~/.copilot/pkg (ABI skew). - writer.WriteStringValue("--no-auto-update"); foreach (var arg in args) { writer.WriteStringValue(arg); diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 7d076d14b0..5995abaaff 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -977,7 +977,7 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission } catch (Exception ex) { - _logger.LogError(ex, "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}", SessionId, requestId); + LogPermissionHandlerOrDeliveryFailed(ex, SessionId, requestId); try { await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, PermissionDecision.UserNotAvailable()); @@ -1975,6 +1975,9 @@ await InvokeRpcAsync( [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to fetch tool metadata for {toolName}")] private partial void LogToolMetadataFetchFailed(Exception exception, string toolName); + [LoggerMessage(Level = LogLevel.Error, Message = "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}")] + private partial void LogPermissionHandlerOrDeliveryFailed(Exception exception, string sessionId, string requestId); + internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty; diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index 5f7944b2c4..95770dba8e 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -38,6 +38,8 @@ <_CopilotPlatform Condition="'$(_CopilotRid)' == 'osx-arm64'">darwin-arm64 <_CopilotBinary Condition="$(_CopilotRid.StartsWith('win-'))">copilot.exe <_CopilotBinary Condition="'$(_CopilotBinary)' == ''">copilot + <_CopilotRuntimeWrapper Condition="$(_CopilotRid.StartsWith('win-'))">copilot-runtime.exe + <_CopilotRuntimeWrapper Condition="'$(_CopilotRuntimeWrapper)' == ''">copilot-runtime <_CopilotRuntimeNodePath>$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\runtime.node + <_CopilotRuntimeWrapperPath Condition="'$(_CopilotRuntimeWrapperPath)' == ''">$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\$(_CopilotRuntimeWrapper) + <_CopilotRuntimeAssetManifest>$(_CopilotOutputDir)\.copilot-runtime-assets + <_CopilotExplicitCliMarker>$(_CopilotOutputDir)\.copilot-explicit-cli + + + + + + <_CopilotSafePreviousRuntimeAsset Include="@(_CopilotPreviousRuntimeAsset)" + Condition="!$([System.IO.Path]::IsPathRooted('%(Identity)')) And !$([System.String]::Copy('%(Identity)').Contains('..'))" /> + + + + + <_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*" + Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot-sdk\**\*;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\preloads\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sdk\**\*;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" /> + <_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*" + Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*" /> + + + + + + + + + diff --git a/dotnet/test/E2E/BuiltinToolsE2ETests.cs b/dotnet/test/E2E/BuiltinToolsE2ETests.cs index 37067ecc60..143487dac4 100644 --- a/dotnet/test/E2E/BuiltinToolsE2ETests.cs +++ b/dotnet/test/E2E/BuiltinToolsE2ETests.cs @@ -112,21 +112,9 @@ public async Task Should_Create_A_New_File() Assert.Contains("Created by test", msg?.Data.Content ?? string.Empty); } - // TODO(cli-1.0.81-2): the grep and glob built-in tools shell out to the CLI's - // bundled ripgrep, which the runtime cannot locate when it is loaded in-process - // over FFI ("Failed to execute ripgrep: No such file or directory"). The tool - // then returns an error the recorded snapshots do not cover. Re-enable once the - // in-process runtime resolves its bundled binaries. - private static bool RipgrepUnavailable => E2ETestContext.UsesInProcessTransport; - [Fact] public async Task Should_Search_For_Patterns_In_Files() { - if (RipgrepUnavailable) - { - return; - } - await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "data.txt"), "apple\nbanana\napricot\ncherry\n"); var session = await CreateSessionAsync(); var msg = await session.SendAndWaitAsync(new MessageOptions @@ -141,11 +129,6 @@ public async Task Should_Search_For_Patterns_In_Files() [Fact] public async Task Should_Find_Files_By_Pattern() { - if (RipgrepUnavailable) - { - return; - } - Directory.CreateDirectory(Path.Join(Ctx.WorkDir, "src")); await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "src", "index.ts"), "export const index = 1;"); await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "README.md"), "# Readme"); diff --git a/dotnet/test/E2E/ClientE2ETests.cs b/dotnet/test/E2E/ClientE2ETests.cs index b6bdfd90fd..282cc9ee67 100644 --- a/dotnet/test/E2E/ClientE2ETests.cs +++ b/dotnet/test/E2E/ClientE2ETests.cs @@ -41,9 +41,8 @@ public async Task Should_Start_And_Connect_To_Server(bool useStdio) [Fact] public async Task Should_Start_And_Connect_Over_InProcess_Ffi() { - // In-process FFI hosting resolves the CLI entrypoint (COPILOT_CLI_PATH or the - // bundled CLI binary) and its sibling native runtime library itself; if neither - // is available, StartAsync throws and the test fails hard. + // In-process FFI hosting loads the bundled runtime library directly; if it is + // unavailable, StartAsync throws and the test fails hard. using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForInProcess(), @@ -193,15 +192,18 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start(bool u var ex = await Assert.ThrowsAsync(() => client.StartAsync()); var errorMessage = ex.Message; - // On .NET Framework with stdio transport, the pipe error may not include stderr content. - if (errorMessage.Contains("pipe", StringComparison.OrdinalIgnoreCase)) +#if NETFRAMEWORK + // .NET Framework can surface the stdio pipe failure before redirected stderr is readable. + if (useStdio) { - // .NET Framework pipe behavior — just verify we got an IOException - Assert.Contains("pipe", errorMessage, StringComparison.OrdinalIgnoreCase); + Assert.True( + errorMessage.Contains("CLI", StringComparison.OrdinalIgnoreCase) + || errorMessage.Contains("pipe", StringComparison.OrdinalIgnoreCase), + $"Expected a CLI process or pipe error, got: {errorMessage}"); } else +#endif { - // Verify we get the stderr output in the error message Assert.Contains("stderr", errorMessage, StringComparison.OrdinalIgnoreCase); Assert.Contains("nonexistent", errorMessage, StringComparison.OrdinalIgnoreCase); } diff --git a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs index b3ca218190..bf1ed687c1 100644 --- a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs +++ b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs @@ -196,7 +196,30 @@ await session1.SendAsync(new MessageOptions if (disconnectOriginalClient) { + await using var lockObserver = Ctx.CreateClient(options: new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + }); + await lockObserver.StartAsync(); + await TestHelper.WaitForConditionAsync( + async () => + { + var result = await lockObserver.Rpc.Sessions.CheckInUseAsync([sessionId]); + return result.InUse.Contains(sessionId); + }, + timeout: PendingWorkTimeout, + timeoutMessage: $"Timed out waiting for session '{sessionId}' to acquire its lock."); + await suspendedClient.ForceStopAsync(); + + await TestHelper.WaitForConditionAsync( + async () => + { + var result = await lockObserver.Rpc.Sessions.CheckInUseAsync([sessionId]); + return !result.InUse.Contains(sessionId); + }, + timeout: PendingWorkTimeout, + timeoutMessage: $"Timed out waiting for session '{sessionId}' to release its lock."); } await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); diff --git a/dotnet/test/E2E/RewindE2ETests.cs b/dotnet/test/E2E/RewindE2ETests.cs index 74c0b8ab9f..3152b2e908 100644 --- a/dotnet/test/E2E/RewindE2ETests.cs +++ b/dotnet/test/E2E/RewindE2ETests.cs @@ -13,26 +13,37 @@ public class RewindE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "rewind", output) { private const string FileName = "rewind-sdk.txt"; + private const string OriginalFileContent = "Original rewind content"; + private const string PreparedFileContent = "Prepared rewind content"; private const string FileContent = "SDK rewind content"; [Fact] public async Task Should_Restore_Tracked_File_And_Conversation() { - // TODO(cli-1.0.81): Re-enable when Windows file-change tracking records built-in create tool writes. - if (OperatingSystem.IsWindows()) - return; - var filePath = Path.Join(Ctx.WorkDir, FileName); + await File.WriteAllTextAsync(filePath, OriginalFileContent); await using var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5", EnableFileChangeTracking = true, }); + var ready = await session.SendAndWaitAsync( + new MessageOptions + { + Prompt = $"Use the edit tool to replace the exact contents of {FileName} " + + $"from {OriginalFileContent} to {PreparedFileContent}. " + + "After the tool succeeds, reply with exactly SDK_REWIND_READY.", + }, + TimeSpan.FromSeconds(30)); + Assert.Equal("SDK_REWIND_READY", ready?.Data.Content); + Assert.Equal(PreparedFileContent, await File.ReadAllTextAsync(filePath)); + var response = await session.SendAndWaitAsync( new MessageOptions { - Prompt = $"Use the create tool to create {FileName} containing exactly {FileContent}. " + Prompt = $"Use the edit tool to replace the exact contents of {FileName} " + + $"from {PreparedFileContent} to {FileContent}. " + "After the tool succeeds, reply with exactly SDK_REWIND_DONE.", }, TimeSpan.FromSeconds(30)); @@ -47,9 +58,10 @@ await TestHelper.WaitForConditionAsync( { rewindPoints = await session.Rpc.History.ListRewindPointsAsync(); return rewindPoints.UnavailableReason is null - && rewindPoints.Points.Count == 1 - && rewindPoints.Points[0].CanRestoreFiles - && rewindPoints.Points[0].FileCount == 1; + && rewindPoints.Points.Count == 2 + && rewindPoints.Points[1].TurnChangedFiles + && rewindPoints.Points[1].CanRestoreFiles + && rewindPoints.Points[1].FileCount == 1; }, timeout: TimeSpan.FromSeconds(30), timeoutMessage: "Timed out waiting for a restorable file rewind point.", @@ -57,7 +69,9 @@ await TestHelper.WaitForConditionAsync( Assert.NotNull(rewindPoints); Assert.True(rewindPoints.FileChangeTrackingEnabled); - var rewindPoint = Assert.Single(rewindPoints.Points); + Assert.Equal(2, rewindPoints.Points.Count); + var rewindPoint = rewindPoints.Points[1]; + Assert.True(rewindPoint.TurnChangedFiles); Assert.True(rewindPoint.CanRestoreFiles); Assert.Equal(1, rewindPoint.FileCount); @@ -80,7 +94,7 @@ await TestHelper.WaitForConditionAsync( Path.GetFullPath(filePath), Path.GetFullPath(restoredFile), OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); - Assert.False(File.Exists(filePath)); + Assert.Equal(PreparedFileContent, await File.ReadAllTextAsync(filePath)); var events = await session.GetEventsAsync(); Assert.DoesNotContain(events, sessionEvent => sessionEvent.Id.ToString() == rewindPoint.EventId); diff --git a/dotnet/test/Unit/MSBuildTargetsTests.cs b/dotnet/test/Unit/MSBuildTargetsTests.cs index a7d9cc0256..cab91e568f 100644 --- a/dotnet/test/Unit/MSBuildTargetsTests.cs +++ b/dotnet/test/Unit/MSBuildTargetsTests.cs @@ -48,6 +48,7 @@ public async Task PreinstalledCliBinaryPath_IsHonored_DownloadSkipped_AndCopiedT var outputPath = sandbox.ExpectedOutputBinary(); Assert.True(File.Exists(outputPath), $"Expected CLI to be copied to '{outputPath}'.\n{result.FailureMessage()}"); Assert.Equal(File.ReadAllText(preinstalled), File.ReadAllText(outputPath)); + Assert.True(File.Exists(Path.Combine(Path.GetDirectoryName(outputPath)!, ".copilot-explicit-cli"))); } [Fact] @@ -105,6 +106,35 @@ public async Task PreinstalledCliBinaryPath_WithSkipCliDownload_StillCopiesToOut Assert.True(File.Exists(sandbox.ExpectedOutputBinary()), result.FailureMessage()); } + [Fact] + public async Task RuntimePackageAssets_AreFilteredAndCopiedToOutput() + { + using var sandbox = MSBuildSandbox.Create(); + var preinstalled = sandbox.WritePreinstalledBinary("fake-cli-contents"); + sandbox.WriteRuntimeCacheAsset("prebuilds", GetNpmPlatform(), "runtime.node", "runtime"); + sandbox.WriteRuntimeCacheAsset("prebuilds", GetNpmPlatform(), + OperatingSystem.IsWindows() ? "copilot-runtime.exe" : "copilot-runtime", "wrapper"); + sandbox.WriteRuntimeCacheAsset("ripgrep", "bin", GetNpmPlatform(), "rg", "ripgrep"); + sandbox.WriteRuntimeCacheAsset("definitions", "future.json", "{}"); + sandbox.WriteRuntimeCacheAsset("app.js", "excluded"); + sandbox.WriteRuntimeCacheAsset("LICENSE.md", "excluded"); + sandbox.WriteRuntimeCacheAsset("README.md", "excluded"); + sandbox.WriteStaleOutputRuntimeAsset("obsolete", "tool", "stale"); + + var result = await sandbox.BuildAsync(new Dictionary + { + ["CopilotCliBinaryPath"] = preinstalled, + }); + + Assert.True(result.Succeeded, result.FailureMessage()); + Assert.Equal("ripgrep", File.ReadAllText(sandbox.ExpectedRuntimeAsset("ripgrep", "bin", GetNpmPlatform(), "rg"))); + Assert.Equal("{}", File.ReadAllText(sandbox.ExpectedRuntimeAsset("definitions", "future.json"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("app.js"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("LICENSE.md"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("README.md"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("obsolete", "tool"))); + } + [Fact] public async Task PreinstalledCliBinaryPath_NonExistentFile_FailsWithActionableError() { @@ -150,6 +180,17 @@ private static string FindTargetsFile([CallerFilePath] string? thisFile = null) "Could not locate GitHub.Copilot.SDK.targets relative to test assembly or source file."); } + private static string GetNpmPlatform() + { + var arch = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture + == System.Runtime.InteropServices.Architecture.Arm64 + ? "arm64" + : "x64"; + if (OperatingSystem.IsWindows()) return $"win32-{arch}"; + if (OperatingSystem.IsMacOS()) return $"darwin-{arch}"; + return $"linux-{arch}"; + } + /// /// A throwaway directory containing a minimal csproj that imports the SDK targets /// file. Disposing removes the directory tree. @@ -203,6 +244,42 @@ public string ExpectedOutputBinary() return Path.Combine(ProjectDir, "bin", "Debug", "net8.0", "runtimes", rid, "native", BinaryName); } + public void WriteRuntimeCacheAsset(params string[] pathAndContents) + { + var pathParts = pathAndContents.Take(pathAndContents.Length - 1).ToArray(); + var path = Path.Combine(ProjectDir, "obj", "Debug", "net8.0", "copilot-cli", "0.0.0-test", + GetNpmPlatform()); + foreach (var part in pathParts) + { + path = Path.Combine(path, part); + } + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, pathAndContents[^1]); + } + + public string ExpectedRuntimeAsset(params string[] pathParts) + { + var path = Path.Combine(ProjectDir, "bin", "Debug", "net8.0", "runtimes", GetPortableRid(), "native"); + foreach (var part in pathParts) + { + path = Path.Combine(path, part); + } + return path; + } + + public void WriteStaleOutputRuntimeAsset(params string[] pathAndContents) + { + var relativeParts = pathAndContents.Take(pathAndContents.Length - 1).ToArray(); + var path = ExpectedRuntimeAsset(relativeParts); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, pathAndContents[^1]); + var manifest = ExpectedRuntimeAsset(".copilot-runtime-assets"); + Directory.CreateDirectory(Path.GetDirectoryName(manifest)!); + File.WriteAllText( + manifest, + string.Join(Path.DirectorySeparatorChar.ToString(), relativeParts) + Environment.NewLine); + } + public async Task BuildAsync(IDictionary properties) { var args = new StringBuilder("build --nologo -clp:NoSummary"); diff --git a/dotnet/test/Unit/RuntimeWrapperTests.cs b/dotnet/test/Unit/RuntimeWrapperTests.cs new file mode 100644 index 0000000000..9a9dacb7d3 --- /dev/null +++ b/dotnet/test/Unit/RuntimeWrapperTests.cs @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.Rpc; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class RuntimeWrapperIsolationCollection +{ + public const string Name = "Runtime wrapper isolation"; +} + +[Collection(RuntimeWrapperIsolationCollection.Name)] +public sealed class RuntimeWrapperTests +{ +#if !NETFRAMEWORK + [Fact] + public async Task Managed_Launch_Fails_When_Bundled_Runtime_Pair_Is_Missing() + { + var originalBaseDirectory = AppContext.GetData("APP_CONTEXT_BASE_DIRECTORY"); + var emptyBaseDirectory = Path.Combine( + Path.GetTempPath(), + $"missing-copilot-runtime-{Guid.NewGuid():N}"); + Directory.CreateDirectory(emptyBaseDirectory); + + try + { + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", emptyBaseDirectory); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + Environment = new Dictionary(), + }); + + var exception = await Assert.ThrowsAsync(() => client.StartAsync()); + + Assert.Contains("runtime wrapper not found", exception.Message, StringComparison.OrdinalIgnoreCase); + } + finally + { + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", originalBaseDirectory); + Directory.Delete(emptyBaseDirectory); + } + } +#endif + + [Fact] + public async Task Explicit_Path_Does_Not_Require_Adjacent_Runtime_Node() + { + var explicitPath = Path.Combine( + Path.GetTempPath(), + $"missing-explicit-copilot-{Guid.NewGuid():N}"); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(path: explicitPath), + Environment = new Dictionary(), + }); + + var exception = await Assert.ThrowsAnyAsync(() => client.StartAsync()); + + Assert.DoesNotContain("runtime.node", exception.ToString(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task Copilot_Cli_Path_Does_Not_Require_Adjacent_Runtime_Node() + { + var explicitPath = Path.Combine( + Path.GetTempPath(), + $"missing-environment-copilot-{Guid.NewGuid():N}"); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + Environment = new Dictionary { ["COPILOT_CLI_PATH"] = explicitPath }, + }); + + var exception = await Assert.ThrowsAnyAsync(() => client.StartAsync()); + + Assert.DoesNotContain("runtime.node", exception.ToString(), StringComparison.OrdinalIgnoreCase); + } + +#if !NETFRAMEWORK + [Fact] + public async Task Marked_Bundled_Explicit_Cli_Does_Not_Require_Runtime_Pair() + { + var originalBaseDirectory = AppContext.GetData("APP_CONTEXT_BASE_DIRECTORY"); + var baseDirectory = Path.Combine( + Path.GetTempPath(), + $"explicit-bundled-copilot-{Guid.NewGuid():N}"); + var rid = GetPortableRid(); + var nativeDirectory = Path.Combine(baseDirectory, "runtimes", rid, "native"); + Directory.CreateDirectory(nativeDirectory); + var cliPath = Path.Combine(nativeDirectory, OperatingSystem.IsWindows() ? "copilot.exe" : "copilot"); + await File.WriteAllTextAsync(cliPath, "not an executable"); + await File.WriteAllTextAsync(Path.Combine(nativeDirectory, ".copilot-explicit-cli"), "explicit"); + + try + { + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", baseDirectory); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + Environment = new Dictionary(), + }); + + var exception = await Assert.ThrowsAnyAsync(() => client.StartAsync()); + + Assert.DoesNotContain("runtime wrapper", exception.ToString(), StringComparison.OrdinalIgnoreCase); + Assert.Contains(cliPath, exception.ToString(), StringComparison.OrdinalIgnoreCase); + } + finally + { + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", originalBaseDirectory); + Directory.Delete(baseDirectory, recursive: true); + } + } +#endif + + private static string GetPortableRid() + { + var os = OperatingSystem.IsWindows() ? "win" + : OperatingSystem.IsMacOS() ? "osx" + : "linux"; + var architecture = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture switch + { + System.Runtime.InteropServices.Architecture.X64 => "x64", + System.Runtime.InteropServices.Architecture.Arm64 => "arm64", + _ => throw new PlatformNotSupportedException(), + }; + return $"{os}-{architecture}"; + } +} diff --git a/go/README.md b/go/README.md index 3d8706cec1..6c85f93ae1 100644 --- a/go/README.md +++ b/go/README.md @@ -104,7 +104,7 @@ Follow these steps to embed the CLI: 1. Run `go get -tool github.com/github/copilot-sdk/go/cmd/bundler`. This is a one-time setup step per project. 2. Run `go tool bundler` in your build environment just before building your application. -That's it! When your application calls `copilot.NewClient` without a `Connection` field (or with an empty `StdioConnection{}`) and no `COPILOT_CLI_PATH` environment variable, the SDK will automatically install the embedded CLI to a cache directory and use it for all operations. +That's it! When your application calls `copilot.NewClient` without a `Connection` field (or with an empty `StdioConnection{}`), the SDK automatically installs the embedded `copilot-runtime` executable and adjacent `runtime.node` to a cache directory for managed child-process connections. The bundler prepares the native runtime library required by the [in-process transport](#in-process-transport-experimental). It is included in the application only when building with the `copilot_inprocess` build tag. @@ -138,6 +138,9 @@ Resolution and requirements: always takes precedence. - Set `COPILOT_CLI_PATH` only when using an externally provisioned compatible runtime package; otherwise the bundled runtime is used. No `PATH` lookup is performed. - Embedded runtime versions are isolated in separate cache directories. Start fails loudly if the native runtime is unavailable. +- Managed child-process start fails if the embedded `copilot-runtime` and + `runtime.node` pair is unavailable; explicit paths and `COPILOT_CLI_PATH` + remain direct overrides. - Linux in-process bundles include both glibc and musl runtime packages and select the matching package automatically at startup. - Only one native runtime version may be loaded per process. @@ -195,7 +198,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `URIConnection{URL, ConnectionToken}` — connect to an already-running runtime (no process spawned) - `InProcessConnection{}` — **Experimental.** Host the runtime in-process via the native FFI library instead of spawning a child process. See [In-process transport](#in-process-transport-experimental) below. - When `Path` is empty for stdio/tcp, the SDK uses the bundled CLI (or `COPILOT_CLI_PATH` env var). + When `Path` is empty for stdio/tcp, the SDK uses `COPILOT_CLI_PATH` when set, then the bundled `copilot-runtime` and adjacent `runtime.node`. `StdioConnection` and `TCPConnection` accept an optional connection-level `Env`. Set environment variables via **either** the client-level `Env` option or the connection's `Env`, not both (setting both panics); prefer the connection-level `Env`. - `WorkingDirectory` (string): Working directory for the runtime process (default: current process working directory) diff --git a/go/client.go b/go/client.go index 2dbd7f7e95..1b45a9f142 100644 --- a/go/client.go +++ b/go/client.go @@ -341,6 +341,18 @@ func NewClient(options *ClientOptions) *Client { return client } +func resolveRuntimeExecutable(explicitPath, bundledRuntimePath string) (string, error) { + if explicitPath != "" { + return explicitPath, nil + } + if bundledRuntimePath == "" { + return "", errors.New( + "managed Copilot runtime unavailable: the embedded bundle does not contain copilot-runtime and adjacent runtime.node; regenerate the bundle, provide an explicit path, or set COPILOT_CLI_PATH", + ) + } + return bundledRuntimePath, nil +} + const defaultConnectionEnvVar = "COPILOT_SDK_DEFAULT_CONNECTION" // resolveDefaultConnection selects the transport when no explicit connection @@ -1987,14 +1999,13 @@ func (c *Client) startCLIServer(ctx context.Context) error { return c.startInProcess(ctx) } - cliPath := c.cliPath - if cliPath == "" { - // If no CLI path is provided, attempt to use the embedded CLI if available - cliPath = embeddedcli.Path() + bundledRuntimePath := "" + if c.cliPath == "" { + bundledRuntimePath = embeddedcli.RuntimePath() } - if cliPath == "" { - // Default to "copilot" in PATH if no embedded CLI is available and no custom path is set - cliPath = "copilot" + cliPath, err := resolveRuntimeExecutable(c.cliPath, bundledRuntimePath) + if err != nil { + return err } // Start with user-provided CLIArgs, then add SDK-managed args @@ -2195,24 +2206,21 @@ func (c *Client) startInProcess(ctx context.Context) error { return errors.New("in-process transport unavailable: rebuild with -tags copilot_inprocess on a supported platform") } - runtimePath := c.cliPath - if runtimePath == "" { - // The in-process transport does not resolve a bare command name from PATH - // (unlike the child-process transport). - if p := getEnvValue(c.options.Env, "COPILOT_CLI_PATH"); p != "" { - runtimePath = p - } + cliEntrypoint := c.cliPath + if cliEntrypoint == "" { + cliEntrypoint = getEnvValue(c.options.Env, "COPILOT_CLI_PATH") } + runtimePath := cliEntrypoint if runtimePath == "" { - runtimePath = embeddedcli.Path() + runtimePath = embeddedcli.RuntimePath() } if runtimePath == "" { - return errors.New("in-process runtime unavailable: set COPILOT_CLI_PATH to a compatible runtime package or build with the bundled embedded runtime") + return errors.New("in-process runtime unavailable: build with the bundled embedded runtime or set COPILOT_CLI_PATH to a compatible runtime package") } config := c.inProcessHostConfig() - host, err := createInProcessHost(runtimePath, config) + host, err := createInProcessHost(runtimePath, cliEntrypoint, config) if err != nil { return err } diff --git a/go/client_test.go b/go/client_test.go index b701de437f..d49aff1cc1 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -24,6 +24,35 @@ import ( // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.go instead +func TestResolveRuntimeExecutable(t *testing.T) { + t.Run("managed launch requires bundled runtime pair", func(t *testing.T) { + _, err := resolveRuntimeExecutable("", "") + if err == nil || !strings.Contains(err.Error(), "copilot-runtime and adjacent runtime.node") { + t.Fatalf("expected missing managed runtime error, got %v", err) + } + }) + + t.Run("explicit path does not require bundled runtime pair", func(t *testing.T) { + path, err := resolveRuntimeExecutable("/explicit/copilot", "") + if err != nil { + t.Fatal(err) + } + if path != "/explicit/copilot" { + t.Fatalf("resolveRuntimeExecutable() = %q", path) + } + }) + + t.Run("managed launch selects bundled wrapper", func(t *testing.T) { + path, err := resolveRuntimeExecutable("", "/bundle/copilot-runtime") + if err != nil { + t.Fatal(err) + } + if path != "/bundle/copilot-runtime" { + t.Fatalf("resolveRuntimeExecutable() = %q", path) + } + }) +} + func TestClient_URLParsing(t *testing.T) { t.Run("should parse port-only URL format", func(t *testing.T) { client := NewClient(&ClientOptions{ diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index e63d1fde66..fb11ff8699 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -91,21 +91,20 @@ func main() { fmt.Printf("Building bundle for %s (CLI version %s)\n", *platform, version) - binaryPath, sha256Hash, runtimeArtifactPath, runtimeHash, err := buildBundle(info, version, outputPath, goos) + bundle, err := buildBundle(info, version, outputPath, goos) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } - var muslBinaryPath, muslRuntimeArtifactPath string - var muslBinaryHash, muslRuntimeHash []byte + var muslBundle bundleArtifacts if goos == "linux" { muslInfo := platformInfo{ npmPlatform: strings.Replace(info.npmPlatform, "linux-", "linuxmusl-", 1), binaryName: info.binaryName, } muslOutputPath := filepath.Join(*output, defaultOutputFileName(version, "linuxmusl", goarch, info.binaryName)) - muslBinaryPath, muslBinaryHash, muslRuntimeArtifactPath, muslRuntimeHash, err = buildBundle( + muslBundle, err = buildBundle( muslInfo, version, muslOutputPath, @@ -121,15 +120,23 @@ func main() { if err := generateGoFile( goos, goarch, - binaryPath, + bundle.binaryPath, version, - sha256Hash, - runtimeArtifactPath, - runtimeHash, - muslBinaryPath, - muslBinaryHash, - muslRuntimeArtifactPath, - muslRuntimeHash, + bundle.binaryHash, + bundle.runtimeArtifactPath, + bundle.runtimeHash, + bundle.wrapperArtifactPath, + bundle.wrapperHash, + bundle.assetsArtifactPath, + bundle.assetsHash, + muslBundle.binaryPath, + muslBundle.binaryHash, + muslBundle.runtimeArtifactPath, + muslBundle.runtimeHash, + muslBundle.wrapperArtifactPath, + muslBundle.wrapperHash, + muslBundle.assetsArtifactPath, + muslBundle.assetsHash, "main", ); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) @@ -286,96 +293,137 @@ func isHex(s string) bool { return true } -// buildBundle downloads the CLI binary (and, when the CLI package ships it, the -// native in-process runtime library) and writes them to outputPath's directory. -// It returns the CLI bundle path and hash, plus the runtime-library artifact path -// and hash (both empty when the package does not ship the runtime library). -func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (string, []byte, string, []byte, error) { +type bundleArtifacts struct { + binaryPath string + binaryHash []byte + runtimeArtifactPath string + runtimeHash []byte + wrapperArtifactPath string + wrapperHash []byte + assetsArtifactPath string + assetsHash []byte +} + +// buildBundle downloads the CLI and native runtime artifacts from one platform package. +func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (bundleArtifacts, error) { outputDir := filepath.Dir(outputPath) if outputDir == "" { outputDir = "." } runtimeArtifactPath := filepath.Join(outputDir, runtimeLibArtifactName(cliVersion, info.npmPlatform, goos)) + wrapperArtifactPath := filepath.Join(outputDir, runtimeWrapperArtifactName(cliVersion, info.npmPlatform, info.binaryName)) + assetsArtifactPath := filepath.Join(outputDir, runtimeAssetsArtifactName(cliVersion, info.npmPlatform)) - // Check if output already exists - if _, err := os.Stat(outputPath); err == nil { + if filesExist(outputPath, runtimeArtifactPath, wrapperArtifactPath, assetsArtifactPath) { // Idempotent output avoids re-downloading in CI or local rebuilds. - fmt.Printf("Output %s already exists, skipping download\n", outputPath) - sha256Hash, err := sha256FileFromCompressed(outputPath) + fmt.Printf("Output runtime bundle for %s already exists, skipping download\n", info.npmPlatform) + binaryHash, err := sha256FileFromCompressed(outputPath) if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to hash existing output: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to hash existing output: %w", err) } if err := downloadCLILicense(cliVersion, outputPath); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to download CLI license: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to download CLI license: %w", err) } - // Reuse an existing runtime-library artifact if present. - if _, err := os.Stat(runtimeArtifactPath); err == nil { - runtimeHash, err := sha256FileFromCompressed(runtimeArtifactPath) - if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to hash existing runtime library: %w", err) - } - return outputPath, sha256Hash, runtimeArtifactPath, runtimeHash, nil + runtimeHash, err := sha256FileFromCompressed(runtimeArtifactPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash existing runtime.node: %w", err) } - return outputPath, sha256Hash, "", nil, nil + wrapperHash, err := sha256FileFromCompressed(wrapperArtifactPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash existing runtime wrapper: %w", err) + } + assetsHash, err := sha256File(assetsArtifactPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash existing runtime assets: %w", err) + } + return bundleArtifacts{outputPath, binaryHash, runtimeArtifactPath, runtimeHash, wrapperArtifactPath, wrapperHash, assetsArtifactPath, assetsHash}, nil } + // Create temp directory for download tempDir, err := os.MkdirTemp("", "copilot-bundler-*") if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to create temp dir: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to create temp dir: %w", err) } defer os.RemoveAll(tempDir) - // Download the binary binaryPath, tarballPath, err := downloadCLIBinary(info.npmPlatform, info.binaryName, cliVersion, tempDir) if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to download CLI binary: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to download CLI binary: %w", err) } - // Create output directory if needed if outputDir != "." { if err := os.MkdirAll(outputDir, 0755); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to create output directory: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to create output directory: %w", err) } } - sha256Hash, err := sha256File(binaryPath) + binaryHash, err := sha256File(binaryPath) if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to hash output binary: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to hash output binary: %w", err) } if err := compressZstdFile(binaryPath, outputPath); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to write output binary: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to write output binary: %w", err) } if err := downloadCLILicense(cliVersion, outputPath); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to download CLI license: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to download CLI license: %w", err) } - // Extract the native in-process runtime library from the same tarball, if the - // package ships it (older CLI versions do not). Missing is not an error — the - // generated file simply omits the runtime embed for that platform. rawLibPath := filepath.Join(tempDir, "runtime.node") - found, err := extractOptionalFileFromTarball(tarballPath, tempDir, - "package/prebuilds/"+info.npmPlatform+"/runtime.node", "runtime.node") + if err := extractFileFromTarball( + tarballPath, + tempDir, + "package/prebuilds/"+info.npmPlatform+"/runtime.node", + "runtime.node", + ); err != nil { + return bundleArtifacts{}, fmt.Errorf("runtime package is missing prebuilds/%s/runtime.node: %w", info.npmPlatform, err) + } + runtimeHash, err := sha256File(rawLibPath) if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to extract runtime library: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to hash runtime.node: %w", err) } - var runtimeHash []byte - returnedRuntimeArtifact := "" - if found { - runtimeHash, err = sha256File(rawLibPath) - if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to hash runtime library: %w", err) - } - if err := compressZstdFile(rawLibPath, runtimeArtifactPath); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to write runtime library: %w", err) - } - returnedRuntimeArtifact = runtimeArtifactPath - fmt.Printf("Successfully created %s\n", runtimeArtifactPath) - } else { - fmt.Printf("Package %s does not ship a runtime library; in-process transport unavailable for this platform bundle\n", info.npmPlatform) + if err := compressZstdFile(rawLibPath, runtimeArtifactPath); err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to write runtime.node: %w", err) + } + + wrapperName := runtimeWrapperName(info.binaryName) + rawWrapperPath := filepath.Join(tempDir, wrapperName) + if err := extractFileFromTarball( + tarballPath, + tempDir, + "package/prebuilds/"+info.npmPlatform+"/"+wrapperName, + wrapperName, + ); err != nil { + return bundleArtifacts{}, fmt.Errorf("runtime package is missing prebuilds/%s/%s: %w", info.npmPlatform, wrapperName, err) + } + wrapperHash, err := sha256File(rawWrapperPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash runtime wrapper: %w", err) + } + if err := compressZstdFile(rawWrapperPath, wrapperArtifactPath); err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to write runtime wrapper: %w", err) + } + if err := createRuntimeAssetsArchive(tarballPath, assetsArtifactPath, info); err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to write runtime assets: %w", err) + } + assetsHash, err := sha256File(assetsArtifactPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash runtime assets: %w", err) } fmt.Printf("Successfully created %s\n", outputPath) - return outputPath, sha256Hash, returnedRuntimeArtifact, runtimeHash, nil + fmt.Printf("Successfully created %s\n", runtimeArtifactPath) + fmt.Printf("Successfully created %s\n", wrapperArtifactPath) + fmt.Printf("Successfully created %s\n", assetsArtifactPath) + return bundleArtifacts{outputPath, binaryHash, runtimeArtifactPath, runtimeHash, wrapperArtifactPath, wrapperHash, assetsArtifactPath, assetsHash}, nil +} + +func filesExist(paths ...string) bool { + for _, path := range paths { + if _, err := os.Stat(path); err != nil { + return false + } + } + return true } // runtimeLibArtifactName builds the compressed runtime-library artifact filename. @@ -383,6 +431,117 @@ func runtimeLibArtifactName(version, npmPlatform, goos string) string { return fmt.Sprintf("zcopilotruntime_%s_%s.%s.zst", version, npmPlatform, runtimeLibExt(goos)) } +func runtimeWrapperArtifactName(version, npmPlatform, binaryName string) string { + return fmt.Sprintf("zcopilotruntimewrapper_%s_%s_%s.zst", version, npmPlatform, runtimeWrapperName(binaryName)) +} + +func runtimeAssetsArtifactName(version, npmPlatform string) string { + return fmt.Sprintf("zcopilotruntimeassets_%s_%s.tgz", version, npmPlatform) +} + +func runtimeWrapperName(binaryName string) string { + if filepath.Ext(binaryName) == ".exe" { + return "copilot-runtime.exe" + } + return "copilot-runtime" +} + +var hostlessExcludedTopLevel = map[string]bool{ + "app.js": true, "assets": true, "changelog.json": true, "copilot": true, "copilot.exe": true, + "copilot-sdk": true, "foundry-local-sdk": true, "index.js": true, "napi-oop-runtime": true, + "LICENSE.md": true, "npm-loader.js": true, "package.json": true, "preloads": true, "pvrecorder": true, + "queries": true, "README.md": true, "sdk": true, "sea-loader.js": true, "webview": true, +} + +func hostlessRuntimePath(name, npmPlatform, wrapperName string) (string, bool) { + relative, ok := strings.CutPrefix(name, "package/") + if !ok { + return "", false + } + parts := strings.Split(relative, "/") + topLevel := parts[0] + fileName := parts[len(parts)-1] + if hostlessExcludedTopLevel[topLevel] || + (strings.HasPrefix(topLevel, "tree-sitter") && strings.HasSuffix(topLevel, ".wasm")) || + (strings.HasPrefix(topLevel, "voice-") && strings.HasSuffix(topLevel, ".js")) || + fileName == "cli-native.node" || fileName == "runtime.node" || fileName == wrapperName || + strings.HasPrefix(fileName, "copilot-runtime-bin") { + return "", false + } + for _, part := range parts { + if part == "mediaremote-adapter" { + return "", false + } + } + if topLevel == "prebuilds" { + if len(parts) < 3 || parts[1] != npmPlatform { + return "", false + } + return strings.Join(parts[2:], "/"), true + } + return relative, true +} + +func createRuntimeAssetsArchive(tarballPath, outputPath string, info platformInfo) error { + sourceFile, err := os.Open(tarballPath) + if err != nil { + return err + } + defer sourceFile.Close() + gzipReader, err := gzip.NewReader(sourceFile) + if err != nil { + return err + } + defer gzipReader.Close() + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + defer outputFile.Close() + gzipWriter := gzip.NewWriter(outputFile) + tarWriter := tar.NewWriter(gzipWriter) + count := 0 + sourceTar := tar.NewReader(gzipReader) + for { + header, err := sourceTar.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + if header.Typeflag != tar.TypeReg { + continue + } + destination, include := hostlessRuntimePath( + header.Name, + info.npmPlatform, + runtimeWrapperName(info.binaryName), + ) + if !include { + continue + } + outputHeader := &tar.Header{ + Name: destination, Mode: header.Mode, Size: header.Size, Typeflag: tar.TypeReg, + Uid: 0, Gid: 0, + } + if err := tarWriter.WriteHeader(outputHeader); err != nil { + return err + } + if _, err := io.Copy(tarWriter, sourceTar); err != nil { + return err + } + count++ + } + if count == 0 { + return fmt.Errorf("runtime package contains no retained assets") + } + if err := tarWriter.Close(); err != nil { + return err + } + return gzipWriter.Close() +} + // runtimeLibExt returns the shared-library extension for the target OS. func runtimeLibExt(goos string) string { switch goos { @@ -406,10 +565,18 @@ func generateGoFile( sha256Hash []byte, runtimeArtifactPath string, runtimeHash []byte, + wrapperArtifactPath string, + wrapperHash []byte, + assetsArtifactPath string, + assetsHash []byte, muslBinaryPath string, muslBinaryHash []byte, muslRuntimeArtifactPath string, muslRuntimeHash []byte, + muslWrapperArtifactPath string, + muslWrapperHash []byte, + muslAssetsArtifactPath string, + muslAssetsHash []byte, pkgName string, ) error { binaryName := filepath.Base(binaryPath) @@ -428,12 +595,20 @@ func generateGoFile( licenseName, cliVersion, hashBase64, - "", - nil, - "", - nil, - "", - nil, + runtimeArtifactPath, + runtimeHash, + wrapperArtifactPath, + wrapperHash, + assetsArtifactPath, + assetsHash, + muslBinaryPath, + muslBinaryHash, + muslRuntimeArtifactPath, + muslRuntimeHash, + muslWrapperArtifactPath, + muslWrapperHash, + muslAssetsArtifactPath, + muslAssetsHash, ) if err := os.WriteFile(defaultPath, []byte(defaultContent), 0644); err != nil { return err @@ -449,10 +624,18 @@ func generateGoFile( hashBase64, runtimeArtifactPath, runtimeHash, + wrapperArtifactPath, + wrapperHash, + assetsArtifactPath, + assetsHash, muslBinaryPath, muslBinaryHash, muslRuntimeArtifactPath, muslRuntimeHash, + muslWrapperArtifactPath, + muslWrapperHash, + muslAssetsArtifactPath, + muslAssetsHash, ) if err := os.WriteFile(inProcessPath, []byte(inProcessContent), 0644); err != nil { return err @@ -472,24 +655,48 @@ func generatedGoFileContent( hashBase64, runtimeArtifactPath string, runtimeHash []byte, + wrapperArtifactPath string, + wrapperHash []byte, + assetsArtifactPath string, + assetsHash []byte, muslBinaryPath string, muslBinaryHash []byte, muslRuntimeArtifactPath string, muslRuntimeHash []byte, + muslWrapperArtifactPath string, + muslWrapperHash []byte, + muslAssetsArtifactPath string, + muslAssetsHash []byte, ) string { runtimeEmbed := "" runtimeConfig := "" runtimeReader := "" - if runtimeArtifactPath != "" { + if runtimeArtifactPath != "" && wrapperArtifactPath != "" && assetsArtifactPath != "" { runtimeArtifactName := filepath.Base(runtimeArtifactPath) runtimeHashBase64 := base64.StdEncoding.EncodeToString(runtimeHash) + wrapperArtifactName := filepath.Base(wrapperArtifactPath) + wrapperHashBase64 := base64.StdEncoding.EncodeToString(wrapperHash) + assetsArtifactName := filepath.Base(assetsArtifactPath) + assetsHashBase64 := base64.StdEncoding.EncodeToString(assetsHash) runtimeEmbed = fmt.Sprintf(` //go:embed %s var localEmbeddedCopilotRuntimeLib []byte -`, runtimeArtifactName) + +//go:embed %s +var localEmbeddedCopilotRuntimeExecutable []byte + +//go:embed %s +var localEmbeddedCopilotRuntimeAssets []byte +`, runtimeArtifactName, wrapperArtifactName, assetsArtifactName) runtimeConfig = fmt.Sprintf(` - RuntimeLib: runtimeLibReader(), - RuntimeLibHash: mustDecodeBase64(%q),`, runtimeHashBase64) + RuntimeLib: runtimeLibReader(), + RuntimeLibHash: mustDecodeBase64(%q), + RuntimeNode: runtimeLibReader(), + RuntimeNodeHash: mustDecodeBase64(%q), + RuntimeExecutable: runtimeExecutableReader(), + RuntimeExecutableHash: mustDecodeBase64(%q), + RuntimeAssets: bytes.NewReader(localEmbeddedCopilotRuntimeAssets), + RuntimeAssetsHash: mustDecodeBase64(%q),`, runtimeHashBase64, runtimeHashBase64, wrapperHashBase64, assetsHashBase64) runtimeReader = ` func runtimeLibReader() io.Reader { r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotRuntimeLib)) @@ -498,29 +705,53 @@ func runtimeLibReader() io.Reader { } return r } + +func runtimeExecutableReader() io.Reader { + r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotRuntimeExecutable)) + if err != nil { + panic("failed to create zstd reader: " + err.Error()) + } + return r +} ` } muslEmbed := "" muslConfig := "" muslReaders := "" - if muslBinaryPath != "" && muslRuntimeArtifactPath != "" { + if muslBinaryPath != "" && muslRuntimeArtifactPath != "" && muslWrapperArtifactPath != "" && muslAssetsArtifactPath != "" { muslBinaryName := filepath.Base(muslBinaryPath) muslBinaryHashBase64 := base64.StdEncoding.EncodeToString(muslBinaryHash) muslRuntimeName := filepath.Base(muslRuntimeArtifactPath) muslRuntimeHashBase64 := base64.StdEncoding.EncodeToString(muslRuntimeHash) + muslWrapperName := filepath.Base(muslWrapperArtifactPath) + muslWrapperHashBase64 := base64.StdEncoding.EncodeToString(muslWrapperHash) + muslAssetsName := filepath.Base(muslAssetsArtifactPath) + muslAssetsHashBase64 := base64.StdEncoding.EncodeToString(muslAssetsHash) muslEmbed = fmt.Sprintf(` //go:embed %s var localEmbeddedCopilotCLILinuxMusl []byte //go:embed %s var localEmbeddedCopilotRuntimeLibLinuxMusl []byte -`, muslBinaryName, muslRuntimeName) + +//go:embed %s +var localEmbeddedCopilotRuntimeExecutableLinuxMusl []byte + +//go:embed %s +var localEmbeddedCopilotRuntimeAssetsLinuxMusl []byte +`, muslBinaryName, muslRuntimeName, muslWrapperName, muslAssetsName) muslConfig = fmt.Sprintf(` - LinuxMuslCli: linuxMuslCLIReader(), - LinuxMuslCliHash: mustDecodeBase64(%q), - LinuxMuslRuntimeLib: linuxMuslRuntimeLibReader(), - LinuxMuslRuntimeLibHash: mustDecodeBase64(%q),`, muslBinaryHashBase64, muslRuntimeHashBase64) + LinuxMuslCli: linuxMuslCLIReader(), + LinuxMuslCliHash: mustDecodeBase64(%q), + LinuxMuslRuntimeLib: linuxMuslRuntimeLibReader(), + LinuxMuslRuntimeLibHash: mustDecodeBase64(%q), + LinuxMuslRuntimeNode: linuxMuslRuntimeLibReader(), + LinuxMuslRuntimeNodeHash: mustDecodeBase64(%q), + LinuxMuslRuntimeExecutable: linuxMuslRuntimeExecutableReader(), + LinuxMuslRuntimeExecutableHash: mustDecodeBase64(%q), + LinuxMuslRuntimeAssets: bytes.NewReader(localEmbeddedCopilotRuntimeAssetsLinuxMusl), + LinuxMuslRuntimeAssetsHash: mustDecodeBase64(%q),`, muslBinaryHashBase64, muslRuntimeHashBase64, muslRuntimeHashBase64, muslWrapperHashBase64, muslAssetsHashBase64) muslReaders = ` func linuxMuslCLIReader() io.Reader { r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotCLILinuxMusl)) @@ -537,6 +768,14 @@ func linuxMuslRuntimeLibReader() io.Reader { } return r } + +func linuxMuslRuntimeExecutableReader() io.Reader { + r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotRuntimeExecutableLinuxMusl)) + if err != nil { + panic("failed to create zstd reader: " + err.Error()) + } + return r +} ` } diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go index badc791359..0a7f1c3876 100644 --- a/go/cmd/bundler/main_test.go +++ b/go/cmd/bundler/main_test.go @@ -1,26 +1,126 @@ package main import ( + "archive/tar" + "bytes" + "compress/gzip" "go/parser" "go/token" + "io" "os" "path/filepath" "strings" "testing" ) -func TestGenerateGoFileGatesRuntimeEmbed(t *testing.T) { +func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *testing.T) { + dir := t.TempDir() + source := filepath.Join(dir, "package.tgz") + output := filepath.Join(dir, "assets.tgz") + writeTarGz(t, source, map[string]string{ + "package/prebuilds/linux-x64/runtime.node": "runtime", + "package/prebuilds/linux-x64/copilot-runtime": "wrapper", + "package/ripgrep/bin/linux-x64/rg": "ripgrep", + "package/definitions/future.json": "{}", + "package/app.js": "excluded", + "package/LICENSE.md": "excluded", + "package/README.md": "excluded", + }) + + if err := createRuntimeAssetsArchive(source, output, platformInfo{ + npmPlatform: "linux-x64", + binaryName: "copilot", + }); err != nil { + t.Fatal(err) + } + + files := readTarGz(t, output) + if files["ripgrep/bin/linux-x64/rg"] != "ripgrep" || files["definitions/future.json"] != "{}" { + t.Fatalf("retained assets = %#v", files) + } + for _, excluded := range []string{ + "runtime.node", "copilot-runtime", "app.js", "LICENSE.md", "README.md", + } { + if _, ok := files[excluded]; ok { + t.Fatalf("excluded asset %q was retained", excluded) + } + } +} + +func writeTarGz(t *testing.T, path string, files map[string]string) { + t.Helper() + var buffer bytes.Buffer + gzipWriter := gzip.NewWriter(&buffer) + tarWriter := tar.NewWriter(gzipWriter) + for name, content := range files { + header := &tar.Header{Name: name, Mode: 0755, Size: int64(len(content)), Typeflag: tar.TypeReg} + if err := tarWriter.WriteHeader(header); err != nil { + t.Fatal(err) + } + if _, err := tarWriter.Write([]byte(content)); err != nil { + t.Fatal(err) + } + } + if err := tarWriter.Close(); err != nil { + t.Fatal(err) + } + if err := gzipWriter.Close(); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, buffer.Bytes(), 0644); err != nil { + t.Fatal(err) + } +} + +func readTarGz(t *testing.T, path string) map[string]string { + t.Helper() + file, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer file.Close() + gzipReader, err := gzip.NewReader(file) + if err != nil { + t.Fatal(err) + } + files := map[string]string{} + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + return files + } + if err != nil { + t.Fatal(err) + } + content, err := io.ReadAll(tarReader) + if err != nil { + t.Fatal(err) + } + files[header.Name] = string(content) + } +} + +func TestGenerateGoFileEmbedsRuntimeWrapperPair(t *testing.T) { dir := t.TempDir() binaryPath := filepath.Join(dir, "copilot.zst") runtimePath := filepath.Join(dir, "runtime.node.zst") + wrapperPath := filepath.Join(dir, "copilot-runtime.zst") + assetsPath := filepath.Join(dir, "runtime-assets.tgz") muslBinaryPath := filepath.Join(dir, "copilot-musl.zst") muslRuntimePath := filepath.Join(dir, "runtime-musl.node.zst") + muslWrapperPath := filepath.Join(dir, "copilot-runtime-musl.zst") + muslAssetsPath := filepath.Join(dir, "runtime-assets-musl.tgz") for _, path := range []string{ binaryPath, licensePathForOutput(binaryPath), runtimePath, + wrapperPath, + assetsPath, muslBinaryPath, muslRuntimePath, + muslWrapperPath, + muslAssetsPath, } { if err := os.WriteFile(path, []byte("test"), 0644); err != nil { t.Fatal(err) @@ -36,10 +136,18 @@ func TestGenerateGoFileGatesRuntimeEmbed(t *testing.T) { hash, runtimePath, hash, + wrapperPath, + hash, + assetsPath, + hash, muslBinaryPath, hash, muslRuntimePath, hash, + muslWrapperPath, + hash, + muslAssetsPath, + hash, "main", ); err != nil { t.Fatal(err) @@ -52,8 +160,20 @@ func TestGenerateGoFileGatesRuntimeEmbed(t *testing.T) { if !strings.Contains(string(defaultSource), "//go:build !copilot_inprocess") { t.Fatal("default embed file does not exclude copilot_inprocess builds") } - if strings.Contains(string(defaultSource), "localEmbeddedCopilotRuntimeLib") { - t.Fatal("default embed file includes the native runtime") + if !strings.Contains(string(defaultSource), "localEmbeddedCopilotRuntimeExecutable") { + t.Fatal("default embed file does not include the runtime wrapper") + } + if !strings.Contains(string(defaultSource), "RuntimeNode:") { + t.Fatal("default embed file does not configure runtime.node") + } + if !strings.Contains(string(defaultSource), "RuntimeAssets:") { + t.Fatal("default embed file does not configure retained runtime assets") + } + if !strings.Contains(string(defaultSource), "localEmbeddedCopilotCLILinuxMusl") { + t.Fatal("default embed file does not include the Linux musl CLI") + } + if !strings.Contains(string(defaultSource), "localEmbeddedCopilotRuntimeLibLinuxMusl") { + t.Fatal("default embed file does not include the Linux musl runtime") } if _, err := parser.ParseFile(token.NewFileSet(), "zcopilot_linux_amd64.go", defaultSource, parser.AllErrors); err != nil { t.Fatalf("default generated source is invalid: %v", err) diff --git a/go/inprocess_disabled.go b/go/inprocess_disabled.go index d0626a74d3..c100e3db6a 100644 --- a/go/inprocess_disabled.go +++ b/go/inprocess_disabled.go @@ -8,6 +8,6 @@ const inProcessAvailable = false var errInProcessUnavailable = errors.New("in-process transport unavailable") -func createInProcessHost(string, inProcessHostConfig) (inProcessHost, error) { +func createInProcessHost(string, string, inProcessHostConfig) (inProcessHost, error) { return nil, errInProcessUnavailable } diff --git a/go/inprocess_enabled.go b/go/inprocess_enabled.go index c20013d8ab..2c30d5f863 100644 --- a/go/inprocess_enabled.go +++ b/go/inprocess_enabled.go @@ -6,6 +6,6 @@ import "github.com/github/copilot-sdk/go/internal/ffihost" const inProcessAvailable = true -func createInProcessHost(runtimePath string, config inProcessHostConfig) (inProcessHost, error) { - return ffihost.Create(runtimePath, config.Environment, config.Args) +func createInProcessHost(runtimePath, cliEntrypoint string, config inProcessHostConfig) (inProcessHost, error) { + return ffihost.Create(runtimePath, cliEntrypoint, config.Environment, config.Args) } diff --git a/go/internal/e2e/inprocess_ffi_e2e_test.go b/go/internal/e2e/inprocess_ffi_e2e_test.go index 6923384a28..7f7dcc3f20 100644 --- a/go/internal/e2e/inprocess_ffi_e2e_test.go +++ b/go/internal/e2e/inprocess_ffi_e2e_test.go @@ -8,8 +8,8 @@ import ( ) // TestInProcessFfiE2E is a smoke test for the in-process (FFI) transport. It -// starts a client that loads the native runtime cdylib next to the resolved CLI -// entrypoint, lets the native host spawn the worker, performs a purely local +// starts a client that loads the native runtime cdylib directly, lets the native +// host construct the server, performs a purely local // "ping" round-trip through the runtime, and stops cleanly. No auth or replay // proxy is involved, so it needs no snapshot. // diff --git a/go/internal/e2e/pending_work_resume_e2e_test.go b/go/internal/e2e/pending_work_resume_e2e_test.go index 00419aec56..58de8644ac 100644 --- a/go/internal/e2e/pending_work_resume_e2e_test.go +++ b/go/internal/e2e/pending_work_resume_e2e_test.go @@ -468,7 +468,32 @@ func TestPendingWorkResumeE2E(t *testing.T) { } if scenario.disconnectOriginalClient { + lockObserver := ctx.NewClient(func(opts *copilot.ClientOptions) { + stdio := opts.Connection.(copilot.StdioConnection) + opts.Connection = copilot.TCPConnection{Path: stdio.Path} + }) + t.Cleanup(func() { lockObserver.ForceStop() }) + if err := lockObserver.Start(t.Context()); err != nil { + t.Fatalf("Failed to start session lock observer: %v", err) + } + + waitForRPCCondition(t, pendingWorkTimeout, "session lock to be held before disconnect", func() (bool, error) { + result, err := lockObserver.RPC.Sessions.CheckInUse( + t.Context(), + &rpc.SessionsCheckInUseRequest{SessionIDs: []string{sessionID}}, + ) + return err == nil && containsString(result.InUse, sessionID), err + }) + suspendedClient.ForceStop() + + waitForRPCCondition(t, pendingWorkTimeout, "session lock to be released after disconnect", func() (bool, error) { + result, err := lockObserver.RPC.Sessions.CheckInUse( + t.Context(), + &rpc.SessionsCheckInUseRequest{SessionIDs: []string{sessionID}}, + ) + return err == nil && !containsString(result.InUse, sessionID), err + }) } resumedClient := ctx.NewClient(func(opts *copilot.ClientOptions) { diff --git a/go/internal/e2e/rewind_e2e_test.go b/go/internal/e2e/rewind_e2e_test.go index 5fc29a13e8..ab73feadda 100644 --- a/go/internal/e2e/rewind_e2e_test.go +++ b/go/internal/e2e/rewind_e2e_test.go @@ -14,8 +14,10 @@ import ( ) const ( - rewindFileName = "rewind-sdk.txt" - rewindFileContent = "SDK rewind content" + rewindFileName = "rewind-sdk.txt" + rewindFileOriginalContent = "Original rewind content" + rewindFilePreparedContent = "Prepared rewind content" + rewindFileContent = "SDK rewind content" ) func TestRewindE2E(t *testing.T) { @@ -24,12 +26,11 @@ func TestRewindE2E(t *testing.T) { t.Cleanup(func() { client.ForceStop() }) t.Run("should restore tracked file and conversation", func(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("blocked on CLI 1.0.81 file-change tracking regression on Windows") - } - ctx.ConfigureForTest(t) filePath := filepath.Join(ctx.WorkDir, rewindFileName) + if err := os.WriteFile(filePath, []byte(rewindFileOriginalContent), 0o600); err != nil { + t.Fatalf("Failed to create original file: %v", err) + } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ Model: "claude-sonnet-4.5", EnableFileChangeTracking: copilot.Bool(true), @@ -40,9 +41,30 @@ func TestRewindE2E(t *testing.T) { } defer session.Disconnect() + ready, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the edit tool to replace the exact contents of " + rewindFileName + " from " + + rewindFileOriginalContent + " to " + rewindFilePreparedContent + + ". After the tool succeeds, reply with exactly SDK_REWIND_READY.", + }) + if err != nil { + t.Fatalf("SendAndWait readiness turn failed: %v", err) + } + readyData, ok := ready.Data.(*copilot.AssistantMessageData) + if !ok || readyData.Content != "SDK_REWIND_READY" { + t.Fatalf("Expected SDK_REWIND_READY response, got %+v", ready) + } + content, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("Failed to read prepared file: %v", err) + } + if string(content) != rewindFilePreparedContent { + t.Fatalf("Expected file content %q, got %q", rewindFilePreparedContent, content) + } + response, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ - Prompt: "Use the create tool to create " + rewindFileName + " containing exactly " + - rewindFileContent + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE.", + Prompt: "Use the edit tool to replace the exact contents of " + rewindFileName + " from " + + rewindFilePreparedContent + " to " + rewindFileContent + + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE.", }) if err != nil { t.Fatalf("SendAndWait failed: %v", err) @@ -51,9 +73,9 @@ func TestRewindE2E(t *testing.T) { if !ok || responseData.Content != "SDK_REWIND_DONE" { t.Fatalf("Expected SDK_REWIND_DONE response, got %+v", response) } - content, err := os.ReadFile(filePath) + content, err = os.ReadFile(filePath) if err != nil { - t.Fatalf("Failed to read created file: %v", err) + t.Fatalf("Failed to read updated file: %v", err) } if string(content) != rewindFileContent { t.Fatalf("Expected file content %q, got %q", rewindFileContent, content) @@ -63,10 +85,13 @@ func TestRewindE2E(t *testing.T) { if !rewindPoints.FileChangeTrackingEnabled { t.Fatal("Expected file change tracking to be enabled") } - if len(rewindPoints.Points) != 1 { - t.Fatalf("Expected one rewind point, got %+v", rewindPoints.Points) + if len(rewindPoints.Points) != 2 { + t.Fatalf("Expected two rewind points, got %+v", rewindPoints.Points) + } + rewindPoint := rewindPoints.Points[1] + if !rewindPoint.TurnChangedFiles { + t.Fatalf("Expected the edit turn to report changed files, got %+v", rewindPoint) } - rewindPoint := rewindPoints.Points[0] if !rewindPoint.CanRestoreFiles || rewindPoint.FileCount != 1 { t.Fatalf("Expected one restorable file, got %+v", rewindPoint) } @@ -99,8 +124,12 @@ func TestRewindE2E(t *testing.T) { t.Fatalf("Expected one restored file, got %+v", rewind.RestoredFiles) } assertSameRewindPath(t, filePath, rewind.RestoredFiles[0]) - if _, err := os.Stat(filePath); !os.IsNotExist(err) { - t.Fatalf("Expected rewound file to be removed, stat error: %v", err) + content, err = os.ReadFile(filePath) + if err != nil { + t.Fatalf("Failed to read restored file: %v", err) + } + if string(content) != rewindFilePreparedContent { + t.Fatalf("Expected restored file content %q, got %q", rewindFilePreparedContent, content) } events, err := session.GetEvents(t.Context()) @@ -124,9 +153,10 @@ func waitForRewindPoints(t *testing.T, session *copilot.Session) *rpc.HistoryLis t.Fatalf("ListRewindPoints failed: %v", err) } if result.UnavailableReason == nil && - len(result.Points) == 1 && - result.Points[0].CanRestoreFiles && - result.Points[0].FileCount == 1 { + len(result.Points) == 2 && + result.Points[1].TurnChangedFiles && + result.Points[1].CanRestoreFiles && + result.Points[1].FileCount == 1 { return result } if time.Now().After(deadline) { diff --git a/go/internal/embeddedcli/embeddedcli.go b/go/internal/embeddedcli/embeddedcli.go index cd0be21895..2535cf5f20 100644 --- a/go/internal/embeddedcli/embeddedcli.go +++ b/go/internal/embeddedcli/embeddedcli.go @@ -1,7 +1,9 @@ package embeddedcli import ( + "archive/tar" "bytes" + "compress/gzip" "crypto/sha256" "fmt" "io" @@ -23,10 +25,10 @@ import ( // version-specific child directory so multiple versions can coexist. License, // when provided, is written next to the installed binary. // -// RuntimeLib and RuntimeLibHash are optional: when set, the native in-process -// runtime library (cdylib) is installed next to the CLI binary so the in-process -// (FFI) transport can load it. They are omitted for CLI packages that do not -// ship the native runtime. +// RuntimeExecutable and RuntimeNode form the adjacent out-of-process runtime +// pair. RuntimeAssets is a filtered npm package archive containing auxiliary +// binaries and resources. RuntimeLib is the same cdylib bytes installed under +// the natural platform name for the optional in-process transport. type Config struct { Cli io.Reader CliHash []byte @@ -36,12 +38,25 @@ type Config struct { RuntimeLib io.Reader RuntimeLibHash []byte + RuntimeExecutable io.Reader + RuntimeExecutableHash []byte + RuntimeNode io.Reader + RuntimeNodeHash []byte + RuntimeAssets io.Reader + RuntimeAssetsHash []byte + // LinuxMuslCli and LinuxMuslRuntimeLib are optional alternatives selected // automatically when the application runs on a musl-based Linux system. - LinuxMuslCli io.Reader - LinuxMuslCliHash []byte - LinuxMuslRuntimeLib io.Reader - LinuxMuslRuntimeLibHash []byte + LinuxMuslCli io.Reader + LinuxMuslCliHash []byte + LinuxMuslRuntimeLib io.Reader + LinuxMuslRuntimeLibHash []byte + LinuxMuslRuntimeExecutable io.Reader + LinuxMuslRuntimeExecutableHash []byte + LinuxMuslRuntimeNode io.Reader + LinuxMuslRuntimeNodeHash []byte + LinuxMuslRuntimeAssets io.Reader + LinuxMuslRuntimeAssetsHash []byte Dir string Version string @@ -60,6 +75,10 @@ func Setup(cfg Config) { if cfg.LinuxMuslRuntimeLib != nil && len(cfg.LinuxMuslRuntimeLibHash) != sha256.Size { panic(fmt.Sprintf("LinuxMuslRuntimeLibHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(cfg.LinuxMuslRuntimeLibHash))) } + validateRuntimePairConfig(cfg.RuntimeExecutable, cfg.RuntimeExecutableHash, cfg.RuntimeNode, cfg.RuntimeNodeHash, "") + validateRuntimePairConfig(cfg.LinuxMuslRuntimeExecutable, cfg.LinuxMuslRuntimeExecutableHash, cfg.LinuxMuslRuntimeNode, cfg.LinuxMuslRuntimeNodeHash, "LinuxMusl") + validateOptionalHash(cfg.RuntimeAssets, cfg.RuntimeAssetsHash, "RuntimeAssetsHash") + validateOptionalHash(cfg.LinuxMuslRuntimeAssets, cfg.LinuxMuslRuntimeAssetsHash, "LinuxMuslRuntimeAssetsHash") setupMu.Lock() defer setupMu.Unlock() if setupDone { @@ -93,13 +112,34 @@ func RuntimeLibPath() string { return runtimeLibPath } +// RuntimePath returns the installed copilot-runtime executable, or "" when the +// application bundle predates the out-of-process runtime pair. +func RuntimePath() string { + setupMu.Lock() + defer setupMu.Unlock() + if !setupDone { + return "" + } + pathInitialized = true + selectLinuxMuslBundle() + if config.RuntimeExecutable == nil { + return "" + } + if runtimePath == "" { + runtimePath = installRuntime() + } + return runtimePath +} + var ( - config Config - setupMu sync.Mutex - setupDone bool - pathInitialized bool - runtimeLibPath string - linuxMuslBundle bool + config Config + setupMu sync.Mutex + setupDone bool + pathInitialized bool + runtimeLibPath string + runtimePath string + runtimeAssetsInstalled bool + linuxMuslBundle bool ) func install() (path string) { @@ -118,6 +158,38 @@ func install() (path string) { fmt.Printf("installing embedded CLI at %s installation took %s\n", path, duration) }() } + installDir := configuredInstallDir() + path, err := installAt(installDir) + if err != nil { + logError("installing in configured directory", err) + return "" + } + return path +} + +func installRuntime() (path string) { + verbose := os.Getenv("COPILOT_CLI_INSTALL_VERBOSE") == "1" + logError := func(msg string, err error) { + if verbose { + fmt.Printf("embedded runtime installation error: %s: %v\n", msg, err) + } + } + if verbose { + start := time.Now() + defer func() { + fmt.Printf("installing embedded runtime at %s took %s\n", path, time.Since(start)) + }() + } + + path, err := installRuntimeAt(configuredInstallDir()) + if err != nil { + logError("installing in configured directory", err) + return "" + } + return path +} + +func configuredInstallDir() string { installDir := config.Dir if installDir == "" { if copilotHome := os.Getenv("COPILOT_HOME"); copilotHome != "" { @@ -131,12 +203,7 @@ func install() (path string) { installDir = filepath.Join(installDir, "copilot-sdk") } } - path, err := installAt(installDir) - if err != nil { - logError("installing in configured directory", err) - return "" - } - return path + return installDir } func selectLinuxMuslBundle() { @@ -152,6 +219,12 @@ func linuxMuslConfig(cfg Config) Config { cfg.CliHash = cfg.LinuxMuslCliHash cfg.RuntimeLib = cfg.LinuxMuslRuntimeLib cfg.RuntimeLibHash = cfg.LinuxMuslRuntimeLibHash + cfg.RuntimeExecutable = cfg.LinuxMuslRuntimeExecutable + cfg.RuntimeExecutableHash = cfg.LinuxMuslRuntimeExecutableHash + cfg.RuntimeNode = cfg.LinuxMuslRuntimeNode + cfg.RuntimeNodeHash = cfg.LinuxMuslRuntimeNodeHash + cfg.RuntimeAssets = cfg.LinuxMuslRuntimeAssets + cfg.RuntimeAssetsHash = cfg.LinuxMuslRuntimeAssetsHash return cfg } @@ -198,6 +271,9 @@ func installAt(installDir string) (string, error) { } runtimeLibPath = libPath } + if err := installRuntimeAssets(installDir); err != nil { + return "", err + } return finalPath, nil } @@ -229,12 +305,188 @@ func installAt(installDir string) (string, error) { if err != nil { return "", err } + if err := installRuntimeAssets(installDir); err != nil { + return "", err + } runtimeLibPath = libPath } return finalPath, nil } +func installRuntimeAt(installDir string) (string, error) { + version := sanitizeVersion(config.Version) + if version != "" { + installDir = filepath.Join(installDir, version) + } + if linuxMuslBundle { + installDir = filepath.Join(installDir, "linuxmusl") + } + if err := os.MkdirAll(installDir, 0755); err != nil { + return "", fmt.Errorf("creating install directory: %w", err) + } + + if release, _ := flock.Acquire(filepath.Join(installDir, ".copilot-cli.lock")); release != nil { + defer release() + } + path, err := installRuntimePair(installDir) + if err != nil { + return "", err + } + if err := installRuntimeAssets(installDir); err != nil { + return "", err + } + return path, nil +} + +func validateOptionalHash(reader io.Reader, hash []byte, name string) { + if reader != nil && len(hash) != sha256.Size { + panic(fmt.Sprintf("%s must be a SHA-256 hash (%d bytes), got %d bytes", name, sha256.Size, len(hash))) + } +} + +func installRuntimeAssets(installDir string) error { + if config.RuntimeAssets == nil || runtimeAssetsInstalled { + return nil + } + archiveBytes, err := io.ReadAll(config.RuntimeAssets) + if closer, ok := config.RuntimeAssets.(io.Closer); ok { + closer.Close() + } + if err != nil { + return fmt.Errorf("reading runtime assets: %w", err) + } + actual := sha256.Sum256(archiveBytes) + if !bytes.Equal(actual[:], config.RuntimeAssetsHash) { + return fmt.Errorf("runtime assets hash mismatch") + } + gzipReader, err := gzip.NewReader(bytes.NewReader(archiveBytes)) + if err != nil { + return fmt.Errorf("opening runtime assets: %w", err) + } + defer gzipReader.Close() + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("reading runtime assets: %w", err) + } + if header.Typeflag != tar.TypeReg { + continue + } + clean := filepath.Clean(filepath.FromSlash(header.Name)) + if !filepath.IsLocal(clean) { + return fmt.Errorf("unsafe runtime asset path %q", header.Name) + } + content, err := io.ReadAll(tarReader) + if err != nil { + return fmt.Errorf("reading runtime asset %q: %w", header.Name, err) + } + path := filepath.Join(installDir, clean) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return fmt.Errorf("creating runtime asset directory: %w", err) + } + hash := sha256.Sum256(content) + mode := os.FileMode(header.Mode & 0777) + if err := installVerifiedFile(path, bytes.NewReader(content), hash[:], mode, "runtime asset"); err != nil { + return err + } + } + runtimeAssetsInstalled = true + return nil +} + +func validateRuntimePairConfig(wrapper io.Reader, wrapperHash []byte, node io.Reader, nodeHash []byte, prefix string) { + if (wrapper == nil) != (node == nil) { + panic(prefix + "RuntimeExecutable and " + prefix + "RuntimeNode must be provided together") + } + if wrapper == nil { + return + } + if len(wrapperHash) != sha256.Size { + panic(fmt.Sprintf("%sRuntimeExecutableHash must be a SHA-256 hash (%d bytes), got %d bytes", prefix, sha256.Size, len(wrapperHash))) + } + if len(nodeHash) != sha256.Size { + panic(fmt.Sprintf("%sRuntimeNodeHash must be a SHA-256 hash (%d bytes), got %d bytes", prefix, sha256.Size, len(nodeHash))) + } +} + +func installRuntimePair(installDir string) (string, error) { + nodePath := filepath.Join(installDir, "runtime.node") + if err := installVerifiedFile(nodePath, config.RuntimeNode, config.RuntimeNodeHash, 0644, "runtime.node"); err != nil { + return "", err + } + wrapperPath := filepath.Join(installDir, runtimeExecutableName()) + if err := installVerifiedFile(wrapperPath, config.RuntimeExecutable, config.RuntimeExecutableHash, 0755, "runtime wrapper"); err != nil { + return "", err + } + return wrapperPath, nil +} + +func installVerifiedFile(path string, reader io.Reader, expectedHash []byte, mode os.FileMode, label string) error { + if _, err := os.Stat(path); err == nil { + existingHash, err := hashFile(path) + if err != nil { + return fmt.Errorf("hashing existing %s: %w", label, err) + } + if !bytes.Equal(existingHash, expectedHash) { + return fmt.Errorf("existing %s hash mismatch", label) + } + if runtime.GOOS != "windows" && mode.Perm()&0111 != 0 { + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("checking existing %s permissions: %w", label, err) + } + if info.Mode().Perm()&0111 == 0 { + if err := os.Chmod(path, info.Mode().Perm()|mode.Perm()&0111); err != nil { + return fmt.Errorf("restoring existing %s permissions: %w", label, err) + } + } + } + return nil + } + + tmp, err := os.CreateTemp(filepath.Dir(path), ".copilot-runtime-pair-*.tmp") + if err != nil { + return fmt.Errorf("creating temporary %s: %w", label, err) + } + tmpPath := tmp.Name() + h := sha256.New() + _, err = io.Copy(io.MultiWriter(tmp, h), reader) + if err1 := tmp.Chmod(mode); err1 != nil && err == nil { + err = err1 + } + if err1 := tmp.Close(); err1 != nil && err == nil { + err = err1 + } + if closer, ok := reader.(io.Closer); ok { + closer.Close() + } + if err != nil { + os.Remove(tmpPath) + return fmt.Errorf("writing %s: %w", label, err) + } + if !bytes.Equal(h.Sum(nil), expectedHash) { + os.Remove(tmpPath) + return fmt.Errorf("%s hash mismatch", label) + } + if err := os.Rename(tmpPath, path); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("installing %s: %w", label, err) + } + return nil +} + +func runtimeExecutableName() string { + if runtime.GOOS == "windows" { + return "copilot-runtime.exe" + } + return "copilot-runtime" +} + // installRuntimeLib writes the embedded runtime cdylib into installDir under its // natural platform file name, verifying its SHA-256. It is idempotent: an // existing file with a matching hash is reused; a mismatch is a hard error. diff --git a/go/internal/embeddedcli/embeddedcli_test.go b/go/internal/embeddedcli/embeddedcli_test.go index b0394e0f69..159b6e1505 100644 --- a/go/internal/embeddedcli/embeddedcli_test.go +++ b/go/internal/embeddedcli/embeddedcli_test.go @@ -1,7 +1,9 @@ package embeddedcli import ( + "archive/tar" "bytes" + "compress/gzip" "crypto/sha256" "os" "path/filepath" @@ -17,9 +19,143 @@ func resetGlobals() { setupDone = false pathInitialized = false runtimeLibPath = "" + runtimePath = "" + runtimeAssetsInstalled = false linuxMuslBundle = false } +func TestInstallRuntimeWritesRetainedAssets(t *testing.T) { + resetGlobals() + tempDir := t.TempDir() + cli := []byte("cli") + wrapper := []byte("wrapper") + node := []byte("runtime") + assets := runtimeAssetsArchive(t, map[string]assetFixture{ + "ripgrep/bin/test-platform/rg": {content: []byte("ripgrep"), mode: 0755}, + "definitions/future.json": {content: []byte("{}"), mode: 0644}, + }) + cliHash := sha256.Sum256(cli) + wrapperHash := sha256.Sum256(wrapper) + nodeHash := sha256.Sum256(node) + assetsHash := sha256.Sum256(assets) + Setup(Config{ + Cli: bytes.NewReader(cli), + CliHash: cliHash[:], + RuntimeExecutable: bytes.NewReader(wrapper), + RuntimeExecutableHash: wrapperHash[:], + RuntimeNode: bytes.NewReader(node), + RuntimeNodeHash: nodeHash[:], + RuntimeAssets: bytes.NewReader(assets), + RuntimeAssetsHash: assetsHash[:], + Version: "1.2.3", + Dir: tempDir, + }) + + gotWrapper, err := installRuntimeAt(tempDir) + if err != nil { + t.Fatal(err) + } + installDir := filepath.Dir(gotWrapper) + if got, err := os.ReadFile(filepath.Join(installDir, "ripgrep", "bin", "test-platform", "rg")); err != nil || string(got) != "ripgrep" { + t.Fatalf("ripgrep content=%q err=%v", got, err) + } + if got, err := os.ReadFile(filepath.Join(installDir, "definitions", "future.json")); err != nil || string(got) != "{}" { + t.Fatalf("definition content=%q err=%v", got, err) + } +} + +type assetFixture struct { + content []byte + mode int64 +} + +func runtimeAssetsArchive(t *testing.T, files map[string]assetFixture) []byte { + t.Helper() + var buffer bytes.Buffer + gzipWriter := gzip.NewWriter(&buffer) + tarWriter := tar.NewWriter(gzipWriter) + for name, fixture := range files { + header := &tar.Header{Name: name, Mode: fixture.mode, Size: int64(len(fixture.content)), Typeflag: tar.TypeReg} + if err := tarWriter.WriteHeader(header); err != nil { + t.Fatal(err) + } + if _, err := tarWriter.Write(fixture.content); err != nil { + t.Fatal(err) + } + } + if err := tarWriter.Close(); err != nil { + t.Fatal(err) + } + if err := gzipWriter.Close(); err != nil { + t.Fatal(err) + } + return buffer.Bytes() +} + +func TestInstallRuntimeWritesAdjacentPairWithoutCLI(t *testing.T) { + resetGlobals() + tempDir := t.TempDir() + cli := []byte("cli") + wrapper := []byte("wrapper") + node := []byte("runtime") + cliHash := sha256.Sum256(cli) + wrapperHash := sha256.Sum256(wrapper) + nodeHash := sha256.Sum256(node) + Setup(Config{ + Cli: bytes.NewReader(cli), + CliHash: cliHash[:], + RuntimeExecutable: bytes.NewReader(wrapper), + RuntimeExecutableHash: wrapperHash[:], + RuntimeNode: bytes.NewReader(node), + RuntimeNodeHash: nodeHash[:], + Version: "1.2.3", + Dir: tempDir, + }) + + gotWrapper, err := installRuntimeAt(tempDir) + if err != nil { + t.Fatal(err) + } + if gotWrapper != filepath.Join(tempDir, "1.2.3", runtimeExecutableName()) { + t.Fatalf("RuntimePath() = %q", gotWrapper) + } + if got, err := os.ReadFile(filepath.Join(filepath.Dir(gotWrapper), "runtime.node")); err != nil || !bytes.Equal(got, node) { + t.Fatalf("runtime.node content=%q err=%v", got, err) + } + if cliPath := filepath.Join(filepath.Dir(gotWrapper), binaryNameForOS()); fileExists(cliPath) { + t.Fatalf("managed runtime installation unexpectedly materialized CLI host at %q", cliPath) + } +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func TestInstallVerifiedFileRestoresExecutablePermission(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows does not use Unix execute bits") + } + path := filepath.Join(t.TempDir(), "copilot-runtime") + content := []byte("wrapper") + hash := sha256.Sum256(content) + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatal(err) + } + + if err := installVerifiedFile(path, bytes.NewReader(content), hash[:], 0755, "runtime wrapper"); err != nil { + t.Fatal(err) + } + + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm()&0111 == 0 { + t.Fatal("existing runtime wrapper was not made executable") + } +} + func mustPanic(t *testing.T, fn func()) { t.Helper() defer func() { diff --git a/go/internal/ffihost/ffihost.go b/go/internal/ffihost/ffihost.go index 30cd831281..9a824881de 100644 --- a/go/internal/ffihost/ffihost.go +++ b/go/internal/ffihost/ffihost.go @@ -159,8 +159,8 @@ type Host struct { // Create resolves the native library and prepares the host. environment and // args contain SDK-managed runtime options. -func Create(cliEntrypoint string, environment map[string]string, args []string) (*Host, error) { - libraryPath, err := ResolveLibraryPath(cliEntrypoint) +func Create(runtimeEntrypoint, cliEntrypoint string, environment map[string]string, args []string) (*Host, error) { + libraryPath, err := ResolveLibraryPath(runtimeEntrypoint) if err != nil { return nil, err } @@ -207,18 +207,13 @@ func (h *Host) Start() error { runtime.KeepAlive(argv) runtime.KeepAlive(env) if h.serverID == 0 { - return fmt.Errorf("copilot_runtime_host_start failed (library %q, entrypoint %q)", h.libraryPath, h.cliEntrypoint) + return fmt.Errorf("copilot_runtime_host_start failed (library %q)", h.libraryPath) } - // host_start spawned the worker child via libuv's uv_spawn, which installs a - // SIGCHLD handler without SA_ONSTACK on its first call. The Go runtime aborts - // ("non-Go code set up signal handler without SA_ONSTACK flag") when it later - // reaps one of its own os/exec children (e.g. a test-spawned MCP server) and - // the delivered SIGCHLD lands on a non-signal stack. Re-add SA_ONSTACK to that - // foreign handler now that it exists (implemented on darwin+linux; a no-op on - // other platforms, and before the first spawn there is nothing to fix — hence - // here rather than at library load). - rearmForeignSignalHandlers(h.lib.handle) + if h.cliEntrypoint != "" { + // A legacy embedded host may install a SIGCHLD handler without SA_ONSTACK. + rearmForeignSignalHandlers(h.lib.handle) + } callbackHandle := sharedOutboundCallback() callbackToken := uintptr(nextOutboundToken.Add(1)) @@ -229,7 +224,9 @@ func (h *Host) Start() error { outboundTargets.Delete(callbackToken) h.callbackToken = 0 h.lib.hostShutdown(h.serverID) - rearmForeignSignalHandlers(h.lib.handle) + if h.cliEntrypoint != "" { + rearmForeignSignalHandlers(h.lib.handle) + } h.serverID = 0 return fmt.Errorf("copilot_runtime_connection_open failed") } @@ -243,14 +240,12 @@ func (h *Host) Writer() io.WriteCloser { return hostWriter{h} } func (h *Host) Reader() io.ReadCloser { return h.recv } func (h *Host) buildArgv() []byte { - // A `.js` entrypoint (dev) is launched via node; the packaged single-file CLI - // embeds its own Node and is invoked directly. `--no-auto-update` pins the - // worker to the runtime package matching the loaded cdylib (avoids ABI skew). - var argv []string - if strings.HasSuffix(strings.ToLower(h.cliEntrypoint), ".js") { - argv = []string{"node", h.cliEntrypoint, "--embedded-host", "--no-auto-update"} - } else { - argv = []string{h.cliEntrypoint, "--embedded-host", "--no-auto-update"} + argv := make([]string, 0, len(h.args)+4) + if h.cliEntrypoint != "" { + if strings.HasSuffix(strings.ToLower(h.cliEntrypoint), ".js") { + argv = append(argv, "node") + } + argv = append(argv, h.cliEntrypoint, "--embedded-host", "--no-auto-update") } argv = append(argv, h.args...) b, _ := json.Marshal(argv) @@ -365,10 +360,10 @@ func (h *Host) Dispose() { } if serverID != 0 { h.lib.hostShutdown(serverID) - // libuv may restore a previously saved SIGCHLD action while tearing down - // its final child watcher, so repair the process-wide handler again after - // shutdown before Go reaps another os/exec child. - rearmForeignSignalHandlers(h.lib.handle) + if h.cliEntrypoint != "" { + // A legacy host may restore its saved SIGCHLD action during shutdown. + rearmForeignSignalHandlers(h.lib.handle) + } } h.recv.Close() } diff --git a/go/internal/ffihost/ffihost_test.go b/go/internal/ffihost/ffihost_test.go index bc588fa6a9..ccb48af419 100644 --- a/go/internal/ffihost/ffihost_test.go +++ b/go/internal/ffihost/ffihost_test.go @@ -25,10 +25,9 @@ func TestDisposeUnregistersOutboundTarget(t *testing.T) { } } -func TestBuildArgvAppendsManagedOptions(t *testing.T) { +func TestBuildArgvWithoutEntrypointContainsOnlyManagedOptions(t *testing.T) { host := &Host{ - cliEntrypoint: "copilot", - args: []string{"--log-level", "debug", "--remote"}, + args: []string{"--log-level", "debug", "--remote"}, } var argv []string @@ -36,7 +35,45 @@ func TestBuildArgvAppendsManagedOptions(t *testing.T) { t.Fatal(err) } - expected := []string{"copilot", "--embedded-host", "--no-auto-update", "--log-level", "debug", "--remote"} + expected := []string{"--log-level", "debug", "--remote"} + if len(argv) != len(expected) { + t.Fatalf("Expected %d arguments, got %d: %v", len(expected), len(argv), argv) + } + for i := range expected { + if argv[i] != expected[i] { + t.Fatalf("Expected argument %d to be %q, got %q", i, expected[i], argv[i]) + } + } +} + +func TestBuildArgvPreservesExplicitEntrypoint(t *testing.T) { + host := &Host{cliEntrypoint: "copilot", args: []string{"--remote"}} + + var argv []string + if err := json.Unmarshal(host.buildArgv(), &argv); err != nil { + t.Fatal(err) + } + + expected := []string{"copilot", "--embedded-host", "--no-auto-update", "--remote"} + if len(argv) != len(expected) { + t.Fatalf("Expected %d arguments, got %d: %v", len(expected), len(argv), argv) + } + for i := range expected { + if argv[i] != expected[i] { + t.Fatalf("Expected argument %d to be %q, got %q", i, expected[i], argv[i]) + } + } +} + +func TestBuildArgvUsesNodeForExplicitJavaScriptEntrypoint(t *testing.T) { + host := &Host{cliEntrypoint: "copilot.js"} + + var argv []string + if err := json.Unmarshal(host.buildArgv(), &argv); err != nil { + t.Fatal(err) + } + + expected := []string{"node", "copilot.js", "--embedded-host", "--no-auto-update"} if len(argv) != len(expected) { t.Fatalf("Expected %d arguments, got %d: %v", len(expected), len(argv), argv) } diff --git a/go/internal/ffihost/resolve.go b/go/internal/ffihost/resolve.go index c8d4052322..5205a01c19 100644 --- a/go/internal/ffihost/resolve.go +++ b/go/internal/ffihost/resolve.go @@ -63,7 +63,8 @@ func PrebuildsFolder() string { // entrypoint. It checks, in order: // // 1. The natural platform library name next to the CLI (bundled/flat layout). -// 2. prebuilds//runtime.node next to the CLI (dev/package layout). +// 2. runtime.node next to the CLI (out-of-process wrapper layout). +// 3. prebuilds//runtime.node next to the CLI (dev/package layout). // // It returns an error when neither exists. func ResolveLibraryPath(cliEntrypoint string) (string, error) { @@ -78,6 +79,11 @@ func ResolveLibraryPath(cliEntrypoint string) (string, error) { return flat, nil } + adjacent := filepath.Join(dir, "runtime.node") + if fileExists(adjacent) { + return adjacent, nil + } + if folder := PrebuildsFolder(); folder != "" { prebuilt := filepath.Join(dir, "prebuilds", folder, "runtime.node") if fileExists(prebuilt) { @@ -86,7 +92,7 @@ func ResolveLibraryPath(cliEntrypoint string) (string, error) { } return "", fmt.Errorf( - "in-process FFI runtime library not found next to %q (looked for %q and prebuilds/%s/runtime.node); "+ + "in-process FFI runtime library not found next to %q (looked for %q, runtime.node, and prebuilds/%s/runtime.node); "+ "use a runtime package that ships the native library", abs, NaturalLibraryName(), PrebuildsFolder()) } diff --git a/java/README.md b/java/README.md index fb4aeb9d25..77b68bdf75 100644 --- a/java/README.md +++ b/java/README.md @@ -20,7 +20,11 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A To use the SDK, you'll need: - Java 17 or later. **JDK 25 recommended**. The distributed jar is a multi-release jar (MR-JAR) and is compiled on JDK 25 with `maven.compiler.release` set to 17. This means, when run on JDK 25 and later, the SDK automatically uses virtual threads for its default internal executor. -- GitHub Copilot CLI 1.0.55-5 or later installed and in `PATH` (or provide custom `cliPath`) + +Managed stdio and TCP connections materialize the platform classifier's +`copilot-runtime[.exe]` and adjacent `runtime.node` by default. An explicit +`cliPath` or `COPILOT_CLI_PATH` environment variable overrides the bundled +runtime. ## Installation @@ -186,9 +190,11 @@ For rotating per-session GitHub credentials, use `ResumeSessionConfig` setter) instead of `setGitHubToken(...)`: ```java -var config = new SessionConfig().setGitHubTokenProvider(args -> - acquireForHost(args.host()).thenApply(token -> - GitHubTokenProviderResult.token(token, 8 * 60 * 60))); +var config = new SessionConfig() + .setGitHubTokenProvider(args -> + acquireForHost(args.host()).thenApply(token -> + GitHubTokenProviderResult.token(token, 8 * 60 * 60))) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL); ``` The remaining lifetime is required and must be positive when the callback @@ -569,7 +575,7 @@ mvn clean verify -Dcopilot.native.libc=glibc mvn clean package -pl copilot-native -DskipTests -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=true ``` -On Linux, the classifier JAR contains `runtime.node`, `platform.properties`, and `copilot` under `native/linux-x64` or `native/linux-arm64`. On Windows, it contains those resources under `native/win32-x64` or `native/win32-arm64`, with the CLI named `copilot.exe`. On Apple Silicon macOS, it contains them under `native/darwin-arm64`. The placeholder JAR remains OS-neutral and contains no native binaries. Unsupported hosts retain the placeholder-only behavior. +Each classifier JAR includes `runtime.node`, `platform.properties`, and `copilot-runtime` (or `copilot-runtime.exe`) under its `native/` directory. It does not contain the legacy `copilot` SEA. The placeholder JAR remains OS-neutral and contains no native binaries. Unsupported hosts retain the placeholder-only behavior. ## License diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index a0272acdc4..21629a33f9 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -126,7 +126,7 @@ @@ -203,12 +203,15 @@ - + + + + - + - + @@ -243,7 +246,6 @@ inprocess linux-x64 - copilot @@ -306,7 +308,6 @@ linux-x64 - copilot @@ -420,7 +421,6 @@ win32-x64 - copilot.exe @@ -530,7 +530,6 @@ darwin-arm64 - copilot diff --git a/java/copilot-native/scripts/create-native-classifier-test-fixture.mjs b/java/copilot-native/scripts/create-native-classifier-test-fixture.mjs index 095d8c1e9e..08eccb644a 100644 --- a/java/copilot-native/scripts/create-native-classifier-test-fixture.mjs +++ b/java/copilot-native/scripts/create-native-classifier-test-fixture.mjs @@ -13,14 +13,14 @@ export function createNativeClassifierTestFixture({ outputPath, repoRoot, }) { - const cliFilename = classifier.startsWith("win32") - ? "copilot.exe" - : "copilot"; + const runtimeFilename = classifier.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; const nativeVersion = readPinnedNativeVersion(repoRoot, classifier); const prefix = `native/${classifier}`; writeStoredZip(outputPath, [ [`${prefix}/runtime.node`, "test runtime"], - [`${prefix}/${cliFilename}`, "test cli"], + [`${prefix}/${runtimeFilename}`, "test runtime wrapper"], [ `${prefix}/platform.properties`, `classifier=${classifier}\nversion=${nativeVersion}\n`, diff --git a/java/copilot-native/scripts/fetch-native.mjs b/java/copilot-native/scripts/fetch-native.mjs index 7b68f04065..4ee91b6aa6 100644 --- a/java/copilot-native/scripts/fetch-native.mjs +++ b/java/copilot-native/scripts/fetch-native.mjs @@ -3,18 +3,16 @@ *--------------------------------------------------------------------------------------------*/ /** - * Downloads the `runtime.node` native binary for a single platform classifier - * and stages it for packaging into a classifier JAR. + * Downloads the native runtime artifacts for one platform classifier. * * Steps: * 1. Read the pinned version and the SHA-512 `integrity` value for * `@github/copilot-` from `nodejs/package-lock.json`. * 2. `npm pack` that exact version into the staging directory. * 3. Verify the downloaded tarball against the `integrity` value. - * 4. Extract `package/prebuilds//runtime.node` to - * `//native//runtime.node`. - * 5. Extract `package/copilot` (or `package/copilot.exe` on Windows) to - * `//native//copilot`. + * 4. Stage the hostless runtime tree, flattening the selected prebuild directory + * beside the package's retained top-level runtime assets. + * 5. Write an inventory consumed by the SDK's generic classpath extractor. * 6. Write `//native//platform.properties`. * * Usage: node fetch-native.mjs @@ -25,6 +23,28 @@ import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +const excludedTopLevel = new Set([ + 'app.js', + 'assets', + 'changelog.json', + 'copilot', + 'copilot.exe', + 'copilot-sdk', + 'foundry-local-sdk', + 'index.js', + 'LICENSE.md', + 'napi-oop-runtime', + 'npm-loader.js', + 'package.json', + 'preloads', + 'pvrecorder', + 'queries', + 'README.md', + 'sdk', + 'sea-loader.js', + 'webview', +]); + const [repoRoot, stagingDir, classifier] = process.argv.slice(2); if (!repoRoot || !stagingDir || !classifier) { @@ -52,34 +72,35 @@ const outDir = path.join(stagingDir, classifier); const resourceDir = path.join(outDir, 'native', classifier); const runtimePath = path.join(resourceDir, 'runtime.node'); const isWindows = classifier.startsWith('win32'); -const cliTarballMember = isWindows ? 'package/copilot.exe' : 'package/copilot'; -const cliFilename = isWindows ? 'copilot.exe' : 'copilot'; -const cliPath = path.join(resourceDir, cliFilename); +const wrapperFilename = isWindows ? 'copilot-runtime.exe' : 'copilot-runtime'; +const wrapperPath = path.join(resourceDir, wrapperFilename); +const inventoryPath = path.join(resourceDir, 'runtime-assets.list'); const platformPropertiesPath = path.join(resourceDir, 'platform.properties'); const expectedPlatformProperties = `classifier=${classifier}\nversion=${version}\n`; +const stagingSchema = 'hostless-runtime-v2'; const stampPath = path.join(outDir, '.version'); // Idempotence: skip the download only when every required staged artifact // matches the package identity recorded in the stamp. if ( fs.existsSync(runtimePath) && - fs.existsSync(cliPath) && + fs.existsSync(wrapperPath) && + fs.existsSync(inventoryPath) && fs.existsSync(platformPropertiesPath) && fs.existsSync(stampPath) ) { const stampLines = fs.readFileSync(stampPath, 'utf8').trim().split('\n'); - const stampVersion = stampLines[0] || ''; - const stampIntegrity = stampLines[1] || ''; - const stampRuntimeDigest = stampLines[2] || ''; - const stampCliDigest = stampLines[3] || ''; - const currentRuntimeDigest = digestFile(runtimePath); - const currentCliDigest = digestFile(cliPath); + const stampSchema = stampLines[0] || ''; + const stampVersion = stampLines[1] || ''; + const stampIntegrity = stampLines[2] || ''; + const stampTreeDigest = stampLines[3] || ''; + const currentTreeDigest = digestTree(resourceDir); const currentPlatformProperties = fs.readFileSync(platformPropertiesPath, 'utf8'); if ( + stampSchema === stagingSchema && stampVersion === version && stampIntegrity === integrity && - stampRuntimeDigest === currentRuntimeDigest && - stampCliDigest === currentCliDigest && + stampTreeDigest === currentTreeDigest && currentPlatformProperties === expectedPlatformProperties ) { console.log(`${packageName}@${version} already staged at ${runtimePath}`); @@ -107,28 +128,99 @@ if (actual !== integrity) { } console.log(`Integrity verified (${integrity.slice(0, 20)}...).`); -const memberPath = `package/prebuilds/${classifier}/runtime.node`; -execFileSync('tar', ['-xzf', tarballPath, '-C', outDir, memberPath], { stdio: 'inherit' }); -fs.renameSync(path.join(outDir, memberPath), runtimePath); - -// Extract the copilot CLI executable (necessary-and-sufficient runtime artifact invariant: -// host_start needs both runtime.node and the copilot CLI from the same package version). -execFileSync('tar', ['-xzf', tarballPath, '-C', outDir, cliTarballMember], { stdio: 'inherit' }); -fs.renameSync(path.join(outDir, cliTarballMember), cliPath); -if (!isWindows) { - fs.chmodSync(cliPath, 0o755); +const inventory = []; +const members = execFileSync('tar', ['-tzf', tarballPath], { encoding: 'utf8' }) + .split(/\r?\n/) + .filter(Boolean); +for (const member of members) { + const destinationRelative = hostlessRuntimePath(member, classifier); + if (destinationRelative === null) { + continue; + } + const listing = execFileSync('tar', ['-tvzf', tarballPath, member], { encoding: 'utf8' }).trim(); + if (listing.startsWith('d')) { + continue; + } + if (!listing.startsWith('-')) { + throw new Error(`Unsupported runtime package entry: ${member}`); + } + const content = execFileSync('tar', ['-xOzf', tarballPath, member], { + encoding: null, + maxBuffer: 512 * 1024 * 1024, + }); + const destination = path.resolve(resourceDir, destinationRelative); + const resourceRoot = `${path.resolve(resourceDir)}${path.sep}`; + if (!destination.startsWith(resourceRoot)) { + throw new Error(`Runtime package entry escapes staging directory: ${member}`); + } + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.writeFileSync(destination, content); + const mode = listing.slice(0, 10).includes('x') ? 0o755 : 0o644; + fs.chmodSync(destination, mode); + inventory.push(`${mode.toString(8)}\t${destinationRelative.split(path.sep).join('/')}`); } +inventory.sort(); +fs.writeFileSync(inventoryPath, `${inventory.join('\n')}\n`); -fs.rmSync(path.join(outDir, 'package'), { recursive: true, force: true }); fs.rmSync(tarballPath, { force: true }); +if (!fs.existsSync(runtimePath) || !fs.existsSync(wrapperPath)) { + throw new Error(`Package ${packageName}@${version} is missing the runtime wrapper pair`); +} fs.writeFileSync(platformPropertiesPath, expectedPlatformProperties); -const runtimeDigest = digestFile(runtimePath); -const cliDigest = digestFile(cliPath); -fs.writeFileSync(stampPath, `${version}\n${integrity}\n${runtimeDigest}\n${cliDigest}\n`); +const treeDigest = digestTree(resourceDir); +fs.writeFileSync(stampPath, `${stagingSchema}\n${version}\n${integrity}\n${treeDigest}\n`); console.log(`Staged ${runtimePath}`); -function digestFile(filePath) { - return `sha512-${createHash('sha512').update(fs.readFileSync(filePath)).digest('base64')}`; +function hostlessRuntimePath(packageRelative, platform) { + if (packageRelative.includes('\\')) { + return null; + } + const parts = packageRelative.split('/'); + if (parts[0] !== 'package' || parts.some((part) => !part || part === '..')) { + return null; + } + parts.shift(); + const topLevel = parts[0]; + const fileName = parts.at(-1); + if ( + excludedTopLevel.has(topLevel) || + (topLevel.startsWith('tree-sitter') && topLevel.endsWith('.wasm')) || + (topLevel.startsWith('voice-') && topLevel.endsWith('.js')) || + fileName === 'cli-native.node' || + parts.includes('mediaremote-adapter') || + fileName.startsWith('copilot-runtime-bin') + ) { + return null; + } + if (topLevel === 'prebuilds') { + if (parts[1] !== platform || parts.length < 3) { + return null; + } + return path.join(...parts.slice(2)); + } + return path.join(...parts); +} + +function walkFiles(directory) { + const files = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...walkFiles(entryPath)); + } else if (entry.isFile()) { + files.push(entryPath); + } + } + return files; +} + +function digestTree(directory) { + const hash = createHash('sha512'); + for (const file of walkFiles(directory).sort()) { + const relative = path.relative(directory, file).split(path.sep).join('/'); + hash.update(relative).update('\0').update(fs.readFileSync(file)).update('\0'); + } + return `sha512-${hash.digest('base64')}`; } diff --git a/java/copilot-native/scripts/fetch-native.test.mjs b/java/copilot-native/scripts/fetch-native.test.mjs index 3ca1e2fde6..582ffa397f 100644 --- a/java/copilot-native/scripts/fetch-native.test.mjs +++ b/java/copilot-native/scripts/fetch-native.test.mjs @@ -7,29 +7,41 @@ import { createHash } from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { spawnSync } from 'node:child_process'; +import { execFileSync, spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import test from 'node:test'; const version = '1.0.79'; const integrity = 'sha512-test-integrity'; const runtimeContent = 'runtime content'; -const cliContent = 'cli content'; +const wrapperContent = 'wrapper content'; +const stagingSchema = 'hostless-runtime-v2'; const scriptPath = fileURLToPath(new URL('./fetch-native.mjs', import.meta.url)); for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64', 'darwin-arm64']) { - test(`${classifier}: missing CLI does not use incremental fast path`, (t) => { + test(`${classifier}: complete hostless artifacts use incremental fast path without a CLI`, (t) => { const fixture = createFixture(t, classifier); - fs.rmSync(fixture.cliPath); + + const result = runScript(fixture); + + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /already staged/); + assert.equal(fs.existsSync(fixture.npmMarkerPath), false); + }); + + test(`${classifier}: missing runtime wrapper does not use incremental fast path`, (t) => { + const fixture = createFixture(t, classifier); + fs.rmSync(fixture.wrapperPath); const result = runScript(fixture); assertRestagingAttempted(fixture, result); }); - test(`${classifier}: stale CLI does not use incremental fast path`, (t) => { + test(`${classifier}: legacy staging schema does not use incremental fast path`, (t) => { const fixture = createFixture(t, classifier); - fs.writeFileSync(fixture.cliPath, 'stale CLI content'); + const stampPath = path.join(fixture.stagingDir, classifier, '.version'); + fs.writeFileSync(stampPath, fs.readFileSync(stampPath, 'utf8').replace(stagingSchema, 'hostless-runtime-v1')); const result = runScript(fixture); @@ -45,6 +57,15 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' assertRestagingAttempted(fixture, result); }); + test(`${classifier}: missing retained runtime asset does not use incremental fast path`, (t) => { + const fixture = createFixture(t, classifier); + fs.rmSync(fixture.ripgrepPath); + + const result = runScript(fixture); + + assertRestagingAttempted(fixture, result); + }); + test(`${classifier}: complete matching artifacts use incremental fast path`, (t) => { const fixture = createFixture(t, classifier); @@ -56,6 +77,55 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' }); } +test('stages retained package assets and excludes CLI-only content', (t) => { + const classifier = 'linux-x64'; + const fixture = createFixture(t, classifier); + const packageRoot = path.join(fixture.repoRoot, 'package-root', 'package'); + fs.mkdirSync(path.join(packageRoot, 'prebuilds', classifier), { recursive: true }); + fs.mkdirSync(path.join(packageRoot, 'ripgrep', 'bin', classifier), { recursive: true }); + fs.mkdirSync(path.join(packageRoot, 'definitions'), { recursive: true }); + fs.writeFileSync(path.join(packageRoot, 'copilot'), 'excluded'); + fs.writeFileSync(path.join(packageRoot, 'prebuilds', classifier, 'runtime.node'), runtimeContent); + fs.writeFileSync(path.join(packageRoot, 'prebuilds', classifier, 'copilot-runtime'), wrapperContent); + fs.writeFileSync(path.join(packageRoot, 'ripgrep', 'bin', classifier, 'rg'), 'ripgrep content'); + fs.chmodSync(path.join(packageRoot, 'ripgrep', 'bin', classifier, 'rg'), 0o755); + fs.writeFileSync(path.join(packageRoot, 'definitions', 'future.json'), '{}'); + fs.writeFileSync(path.join(packageRoot, 'app.js'), 'excluded'); + fs.writeFileSync(path.join(packageRoot, 'LICENSE.md'), 'excluded'); + fs.writeFileSync(path.join(packageRoot, 'README.md'), 'excluded'); + const tarball = path.join(fixture.repoRoot, 'fixture.tgz'); + execFileSync('tar', ['-czf', tarball, '-C', path.dirname(packageRoot), 'package']); + const packageIntegrity = digest(fs.readFileSync(tarball)); + fs.writeFileSync( + path.join(fixture.repoRoot, 'nodejs', 'package-lock.json'), + JSON.stringify({ + packages: { + [`node_modules/@github/copilot-${classifier}`]: { version, integrity: packageIntegrity }, + }, + }), + ); + const fakeNpmPath = path.join(fixture.fakeBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); + const fakeNpm = + process.platform === 'win32' + ? '@copy "%FETCH_NATIVE_TARBALL%" "%4\\fixture.tgz" >nul\r\n@echo fixture.tgz\r\n' + : '#!/bin/sh\ncp "$FETCH_NATIVE_TARBALL" "$4/fixture.tgz"\nprintf "fixture.tgz\\n"\n'; + fs.writeFileSync(fakeNpmPath, fakeNpm); + fs.chmodSync(fakeNpmPath, 0o755); + fs.rmSync(path.join(fixture.stagingDir, classifier), { recursive: true, force: true }); + + const result = runScript(fixture, { FETCH_NATIVE_TARBALL: tarball }); + + assert.equal(result.status, 0, result.stderr); + const resourceDir = path.join(fixture.stagingDir, classifier, 'native', classifier); + assert.equal(fs.readFileSync(path.join(resourceDir, 'ripgrep', 'bin', classifier, 'rg'), 'utf8'), 'ripgrep content'); + assert.equal(fs.readFileSync(path.join(resourceDir, 'definitions', 'future.json'), 'utf8'), '{}'); + assert.equal(fs.existsSync(path.join(resourceDir, 'app.js')), false); + assert.equal(fs.existsSync(path.join(resourceDir, 'copilot')), false); + assert.equal(fs.existsSync(path.join(resourceDir, 'LICENSE.md')), false); + assert.equal(fs.existsSync(path.join(resourceDir, 'README.md')), false); + assert.match(fs.readFileSync(path.join(resourceDir, 'runtime-assets.list'), 'utf8'), /ripgrep\/bin\/linux-x64\/rg/); +}); + function createFixture(t, classifier) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fetch-native-test-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); @@ -79,14 +149,25 @@ function createFixture(t, classifier) { ); const runtimePath = path.join(resourceDir, 'runtime.node'); - const cliPath = path.join(resourceDir, classifier.startsWith('win32') ? 'copilot.exe' : 'copilot'); + const wrapperPath = path.join( + resourceDir, + classifier.startsWith('win32') ? 'copilot-runtime.exe' : 'copilot-runtime', + ); const platformPropertiesPath = path.join(resourceDir, 'platform.properties'); + const ripgrepPath = path.join(resourceDir, 'ripgrep', 'bin', classifier, 'rg'); + const inventoryPath = path.join(resourceDir, 'runtime-assets.list'); + fs.mkdirSync(path.dirname(ripgrepPath), { recursive: true }); fs.writeFileSync(runtimePath, runtimeContent); - fs.writeFileSync(cliPath, cliContent); + fs.writeFileSync(wrapperPath, wrapperContent); + fs.writeFileSync(ripgrepPath, 'ripgrep content'); + fs.writeFileSync( + inventoryPath, + `644\truntime.node\n755\tcopilot-runtime\n755\tripgrep/bin/${classifier}/rg\n`, + ); fs.writeFileSync(platformPropertiesPath, `classifier=${classifier}\nversion=${version}\n`); fs.writeFileSync( path.join(stagingDir, classifier, '.version'), - `${version}\n${integrity}\n${digest(runtimeContent)}\n${digest(cliContent)}\n`, + `${stagingSchema}\n${version}\n${integrity}\n${digestTree(resourceDir)}\n`, ); const fakeNpmPath = path.join(fakeBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); @@ -104,18 +185,20 @@ function createFixture(t, classifier) { fakeBinDir, npmMarkerPath, runtimePath, - cliPath, + wrapperPath, + ripgrepPath, platformPropertiesPath, }; } -function runScript(fixture) { +function runScript(fixture, extraEnv = {}) { return spawnSync(process.execPath, [scriptPath, fixture.repoRoot, fixture.stagingDir, fixture.classifier], { encoding: 'utf8', env: { ...process.env, PATH: `${fixture.fakeBinDir}${path.delimiter}${process.env.PATH}`, FETCH_NATIVE_NPM_MARKER: fixture.npmMarkerPath, + ...extraEnv, }, }); } @@ -125,6 +208,29 @@ function assertRestagingAttempted(fixture, result) { assert.equal(fs.readFileSync(fixture.npmMarkerPath, 'utf8').trim(), 'invoked'); } +function digestTree(directory) { + const hash = createHash('sha512'); + for (const file of walkFiles(directory).sort()) { + const relative = path.relative(directory, file).split(path.sep).join('/'); + hash.update(relative).update('\0').update(fs.readFileSync(file)).update('\0'); + } + + return `sha512-${hash.digest('base64')}`; +} + function digest(content) { return `sha512-${createHash('sha512').update(content).digest('base64')}`; } + +function walkFiles(directory) { + const files = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...walkFiles(entryPath)); + } else { + files.push(entryPath); + } + } + return files; +} diff --git a/java/copilot-native/scripts/validate-native-artifact.mjs b/java/copilot-native/scripts/validate-native-artifact.mjs index dda86a917a..9af4ffd772 100644 --- a/java/copilot-native/scripts/validate-native-artifact.mjs +++ b/java/copilot-native/scripts/validate-native-artifact.mjs @@ -27,13 +27,13 @@ export function validateNativeClassifierJar({ } const archive = readJar(jarPath); - const cliFilename = classifier.startsWith("win32") - ? "copilot.exe" - : "copilot"; + const runtimeFilename = classifier.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; const resourcePrefix = `native/${classifier}/`; const requiredEntries = [ `${resourcePrefix}runtime.node`, - `${resourcePrefix}${cliFilename}`, + `${resourcePrefix}${runtimeFilename}`, `${resourcePrefix}platform.properties`, ]; diff --git a/java/copilot-native/scripts/validate-native-artifact.test.mjs b/java/copilot-native/scripts/validate-native-artifact.test.mjs index eb99d432db..25851bb575 100644 --- a/java/copilot-native/scripts/validate-native-artifact.test.mjs +++ b/java/copilot-native/scripts/validate-native-artifact.test.mjs @@ -181,7 +181,7 @@ test("rejects missing native resources", (t) => { expectedFilename: artifactName, repoRoot: fixture.repoRoot, }), - /copilot\.exe/, + /copilot-runtime\.exe/, ); }); @@ -189,7 +189,7 @@ test("rejects incorrect pinned package metadata", (t) => { const fixture = createFixture(t); writeStoredZip(fixture.jarPath, [ ["native/win32-x64/runtime.node", "runtime"], - ["native/win32-x64/copilot.exe", "cli"], + ["native/win32-x64/copilot-runtime.exe", "runtime wrapper"], [ "native/win32-x64/platform.properties", "classifier=win32-x64\nversion=0.0.1\n", @@ -217,7 +217,7 @@ test("rejects Linux resources in a Windows classifier", (t) => { }); writeStoredZip(fixture.jarPath, [ ["native/win32-x64/runtime.node", "runtime"], - ["native/win32-x64/copilot.exe", "cli"], + ["native/win32-x64/copilot-runtime.exe", "runtime wrapper"], [ "native/win32-x64/platform.properties", "classifier=win32-x64\nversion=9.8.7\n", @@ -245,7 +245,7 @@ test("rejects Windows resources in a Linux classifier", (t) => { const linuxJarPath = path.join(fixture.root, linuxArtifactName); writeStoredZip(linuxJarPath, [ ["native/linux-x64/runtime.node", "runtime"], - ["native/linux-x64/copilot", "cli"], + ["native/linux-x64/copilot-runtime", "runtime wrapper"], [ "native/linux-x64/platform.properties", "classifier=linux-x64\nversion=9.8.6\n", @@ -459,7 +459,7 @@ test("local publication validation rejects cross-classifier contamination", (t) path.join(publicationDirectory, `${artifactId}-${version}-linux-x64.jar`), [ ["native/linux-x64/runtime.node", "runtime"], - ["native/linux-x64/copilot", "cli"], + ["native/linux-x64/copilot-runtime", "runtime wrapper"], [ "native/linux-x64/platform.properties", "classifier=linux-x64\nversion=9.8.6\n", diff --git a/java/sdk/src/main/java/com/github/copilot/CliServerManager.java b/java/sdk/src/main/java/com/github/copilot/CliServerManager.java index acc683a720..ff0d121945 100644 --- a/java/sdk/src/main/java/com/github/copilot/CliServerManager.java +++ b/java/sdk/src/main/java/com/github/copilot/CliServerManager.java @@ -11,6 +11,7 @@ import java.net.Socket; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -19,6 +20,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.github.copilot.ffi.NativeRuntimeLoader; import com.github.copilot.rpc.CopilotClientOptions; /** @@ -64,7 +66,7 @@ void setConnectionToken(String connectionToken) { ProcessInfo startCliServer() throws IOException, InterruptedException { clearStderrBuffer(); - String cliPath = options.getCliPath() != null ? options.getCliPath() : "copilot"; + RuntimeLaunch launch = resolveCliLaunch(); var args = new ArrayList(); if (options.getCliArgs() != null) { @@ -106,7 +108,7 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { args.add("--remote"); } - List command = resolveCliCommand(cliPath, args); + List command = resolveCliCommand(launch.executable(), args); var pb = new ProcessBuilder(command); pb.redirectErrorStream(false); @@ -122,51 +124,7 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { pb.directory(new File(options.getCwd())); } - if (options.getEnvironment() != null) { - pb.environment().clear(); - pb.environment().putAll(options.getEnvironment()); - } - pb.environment().remove("NODE_DEBUG"); - - // Set auth token in environment if provided - if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { - pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); - } - - // Set Copilot home directory if configured - if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) { - pb.environment().put("COPILOT_HOME", options.getCopilotHome()); - } - - // Set connection token for TCP mode - if (connectionToken != null && !connectionToken.isEmpty()) { - pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken); - } - - // Set telemetry environment variables if configured - if (options.getTelemetry() != null) { - var telemetry = options.getTelemetry(); - pb.environment().put("COPILOT_OTEL_ENABLED", "true"); - if (telemetry.getOtlpEndpoint() != null) { - pb.environment().put("OTEL_EXPORTER_OTLP_ENDPOINT", telemetry.getOtlpEndpoint()); - } - if (telemetry.getOtlpProtocol() != null) { - pb.environment().put("OTEL_EXPORTER_OTLP_PROTOCOL", telemetry.getOtlpProtocol()); - } - if (telemetry.getFilePath() != null) { - pb.environment().put("COPILOT_OTEL_FILE_EXPORTER_PATH", telemetry.getFilePath()); - } - if (telemetry.getExporterType() != null) { - pb.environment().put("COPILOT_OTEL_EXPORTER_TYPE", telemetry.getExporterType()); - } - if (telemetry.getSourceName() != null) { - pb.environment().put("COPILOT_OTEL_SOURCE_NAME", telemetry.getSourceName()); - } - if (telemetry.getCaptureContent().isPresent()) { - pb.environment().put("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", - telemetry.getCaptureContent().get() ? "true" : "false"); - } - } + configureProcessEnvironment(pb); Process process = pb.start(); @@ -310,6 +268,75 @@ private List resolveCliCommand(String cliPath, List args) { return result; } + void configureProcessEnvironment(ProcessBuilder pb) { + if (options.getEnvironment() != null) { + pb.environment().clear(); + pb.environment().putAll(options.getEnvironment()); + } + pb.environment().remove("NODE_DEBUG"); + + // Set auth token in environment if provided + if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { + pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); + } + + // Set Copilot home directory if configured + if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) { + pb.environment().put("COPILOT_HOME", options.getCopilotHome()); + } + + // Set connection token for TCP mode + if (connectionToken != null && !connectionToken.isEmpty()) { + pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken); + } + + // Set telemetry environment variables if configured + if (options.getTelemetry() != null) { + var telemetry = options.getTelemetry(); + pb.environment().put("COPILOT_OTEL_ENABLED", "true"); + if (telemetry.getOtlpEndpoint() != null) { + pb.environment().put("OTEL_EXPORTER_OTLP_ENDPOINT", telemetry.getOtlpEndpoint()); + } + if (telemetry.getOtlpProtocol() != null) { + pb.environment().put("OTEL_EXPORTER_OTLP_PROTOCOL", telemetry.getOtlpProtocol()); + } + if (telemetry.getFilePath() != null) { + pb.environment().put("COPILOT_OTEL_FILE_EXPORTER_PATH", telemetry.getFilePath()); + } + if (telemetry.getExporterType() != null) { + pb.environment().put("COPILOT_OTEL_EXPORTER_TYPE", telemetry.getExporterType()); + } + if (telemetry.getSourceName() != null) { + pb.environment().put("COPILOT_OTEL_SOURCE_NAME", telemetry.getSourceName()); + } + if (telemetry.getCaptureContent().isPresent()) { + pb.environment().put("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + telemetry.getCaptureContent().get() ? "true" : "false"); + } + } + } + + RuntimeLaunch resolveCliLaunch() throws IOException { + return resolveCliLaunch(System.getenv(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV)); + } + + RuntimeLaunch resolveCliLaunch(String inheritedCliPath) throws IOException { + if (options.getCliPath() != null) { + return new RuntimeLaunch(options.getCliPath()); + } + + var environment = options.getEnvironment(); + String envCliPath = environment != null + ? environment.get(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV) + : inheritedCliPath; + if (envCliPath != null && !envCliPath.isBlank()) { + return new RuntimeLaunch(envCliPath); + } + + Path wrapper = NativeRuntimeLoader.resolveRuntimeWrapper(); + return new RuntimeLaunch(wrapper.toString()); + } + static URI parseCliUrl(String url) { // If it's just a port number, treat as localhost try { @@ -337,4 +364,7 @@ static URI parseCliUrl(String url) { */ record ProcessInfo(Process process, Integer port) { } + + record RuntimeLaunch(String executable) { + } } diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index ea2b0b67dd..661fe6dab4 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -8,6 +8,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -479,7 +480,8 @@ void setInProcessTransportFactory(InProcessTransportFactory factory) { private static InProcessTransport openInProcessTransport(CopilotClientOptions options) throws IOException { FfiRuntimeHost host = new FfiRuntimeHost(); try { - host.start(resolveInProcessEntrypoint(), options); + Path explicitEntrypoint = NativeRuntimeLoader.resolveConfiguredEntrypoint(); + host.start(explicitEntrypoint == null ? null : explicitEntrypoint.toString(), options); } catch (RuntimeException | Error e) { host.close(); throw e; @@ -487,15 +489,6 @@ private static InProcessTransport openInProcessTransport(CopilotClientOptions op return new InProcessTransport(host.getReceiveStream(), host.getSendStream(), host); } - /** - * Resolves the runtime entrypoint handed to the in-process host. The copilot - * CLI executable is resolved from the same bundled location as - * {@code runtime.node} — no environment variables or PATH search. - */ - private static String resolveInProcessEntrypoint() throws IOException { - return NativeRuntimeLoader.resolveEntrypoint().toString(); - } - private static void closeRuntimeHost(AutoCloseable host) { try { host.close(); diff --git a/java/sdk/src/main/java/com/github/copilot/ffi/FfiRuntimeHost.java b/java/sdk/src/main/java/com/github/copilot/ffi/FfiRuntimeHost.java index 5e7d2d461a..cb5bba1af1 100644 --- a/java/sdk/src/main/java/com/github/copilot/ffi/FfiRuntimeHost.java +++ b/java/sdk/src/main/java/com/github/copilot/ffi/FfiRuntimeHost.java @@ -88,13 +88,13 @@ private static Path resolveLibraryPath() throws IOException { * Starts the in-process runtime and opens a connection. * * @param entrypointPath - * runtime entrypoint path passed in {@code argv_json} + * optional explicit legacy CLI entrypoint passed in + * {@code argv_json} * @param options * client options used to construct {@code argv_json} and * {@code env_json} */ public void start(String entrypointPath, CopilotClientOptions options) { - Objects.requireNonNull(entrypointPath, "entrypointPath must not be null"); Objects.requireNonNull(options, "options must not be null"); if (disposed.get()) { throw new IllegalStateException("FfiRuntimeHost is already closed."); @@ -108,8 +108,7 @@ public void start(String entrypointPath, CopilotClientOptions options) { int hostHandle = runHostStartOnBlockingThread(argvJson, envJson); if (hostHandle == 0) { String lib = libraryPath != null ? libraryPath : ""; - throw new IllegalStateException( - "copilot_runtime_host_start failed (library '" + lib + "', entrypoint '" + entrypointPath + "')."); + throw new IllegalStateException("copilot_runtime_host_start failed (library '" + lib + "')."); } // Hold operationLock while publishing handles to serialize with close(). @@ -270,12 +269,14 @@ private int runHostStartOnBlockingThread(byte[] argvJson, byte[] envJson) { private static byte[] buildArgvJson(String entrypointPath, CopilotClientOptions options) { List argv = new ArrayList<>(); - if (entrypointPath.toLowerCase().endsWith(".js")) { - argv.add("node"); + if (entrypointPath != null) { + if (entrypointPath.toLowerCase().endsWith(".js")) { + argv.add("node"); + } + argv.add(entrypointPath); + argv.add("--embedded-host"); + argv.add("--no-auto-update"); } - argv.add(entrypointPath); - argv.add("--embedded-host"); - argv.add("--no-auto-update"); String logLevel = options.getLogLevel(); if (logLevel != null && !logLevel.isBlank()) { diff --git a/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java b/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java index 6733f4afb2..bd4b185a07 100644 --- a/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java +++ b/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java @@ -5,9 +5,12 @@ package com.github.copilot.ffi; import java.io.FileNotFoundException; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.channels.FileChannel; import java.nio.file.AccessDeniedException; import java.nio.file.AtomicMoveNotSupportedException; @@ -42,7 +45,10 @@ public final class NativeRuntimeLoader { static final String RUNTIME_FILENAME = "runtime.node"; static final String CLI_FILENAME = "copilot"; static final String CLI_FILENAME_WINDOWS = "copilot.exe"; + static final String RUNTIME_WRAPPER_FILENAME = "copilot-runtime"; + static final String RUNTIME_WRAPPER_FILENAME_WINDOWS = "copilot-runtime.exe"; static final String PLATFORM_PROPERTIES_FILENAME = "platform.properties"; + static final String RUNTIME_ASSETS_FILENAME = "runtime-assets.list"; /** Environment variable that overrides where the runtime is loaded from. */ public static final String COPILOT_CLI_PATH_ENV = "COPILOT_CLI_PATH"; static final String VERSION_RESOURCE = "copilot-runtime.properties"; @@ -125,10 +131,10 @@ public static Path resolve() throws IOException { } /** - * Resolves the copilot CLI executable from the same location as the bundled - * {@code runtime.node}. The CLI is used as {@code argv[0]} in - * {@code copilot_runtime_host_start} — the Rust runtime spawns it as a child - * process. + * Resolves the legacy copilot CLI entrypoint from the same location as the + * bundled {@code runtime.node}. Callers may pass this entrypoint through + * {@code copilot_runtime_host_start} when legacy extension hosting is + * requested. * *

* This method calls {@link #resolve()} to locate {@code runtime.node}, then @@ -144,6 +150,73 @@ public static Path resolveEntrypoint() throws IOException { return resolveEntrypoint(configuredCli, resolve()); } + /** + * Resolves an explicitly configured legacy CLI entrypoint, if it has a + * compatible adjacent runtime library. + * + * @return the absolute CLI path, or {@code null} when no compatible override is + * configured + * @throws IOException + * if the configured files cannot be inspected + */ + public static Path resolveConfiguredEntrypoint() throws IOException { + String configuredCli = System.getenv(COPILOT_CLI_PATH_ENV); + if (configuredCli == null || configuredCli.isBlank()) { + return null; + } + Path configuredPath = Path.of(configuredCli).toAbsolutePath().normalize(); + return resolveFromCliPath(configuredCli) != null && Files.isRegularFile(configuredPath) + && Files.size(configuredPath) > 0 ? configuredPath : null; + } + + /** + * Resolves the out-of-process runtime wrapper from the platform classifier JAR + * and extracts it beside {@code runtime.node}. + * + * @return absolute path to the runtime wrapper executable + * @throws IOException + * if the classifier artifacts cannot be extracted + */ + public static Path resolveRuntimeWrapper() throws IOException { + ClassLoader loader = NativeRuntimeLoader.class.getClassLoader(); + String classifier = PlatformDetector.detectClassifier(); + String version = readVersion(loader); + return resolveRuntimeWrapper(defaultCacheBase(), loader, classifier, version); + } + + static Path resolveRuntimeWrapper(Path cacheBase, ClassLoader loader, String classifier, String version) + throws IOException { + Path runtimePath = extractRuntimeToCache(cacheBase, loader, classifier, version, DEFAULT_PUBLISHER, false); + Path cacheDir = runtimePath.getParent(); + String wrapperName = classifier.startsWith("win32-") + ? RUNTIME_WRAPPER_FILENAME_WINDOWS + : RUNTIME_WRAPPER_FILENAME; + Path cachedWrapper = cacheDir.resolve(wrapperName); + if (isValidCachedCli(cachedWrapper)) { + return cachedWrapper; + } + + String resourcePath = "native/" + classifier + "/" + wrapperName; + URL resource = loader.getResource(resourcePath); + if (resource == null) { + throw new FileNotFoundException("Runtime wrapper not found on classpath: " + resourcePath + + " — add the matching classifier JAR to the classpath"); + } + + Path temp = Files.createTempFile(cacheDir, "runtime-wrapper-tmp-", ""); + try { + copyResourceToTemp(resource, resourcePath, temp); + makeExecutable(temp); + DEFAULT_PUBLISHER.publish(temp, cachedWrapper); + } finally { + tryDelete(temp); + } + if (!isValidCachedCli(cachedWrapper)) { + throw new IOException("Published runtime wrapper is not a non-empty executable file: " + cachedWrapper); + } + return cachedWrapper; + } + static Path resolveEntrypoint(String configuredCli, Path runtimePath) throws IOException { if (configuredCli != null && !configuredCli.isBlank()) { Path configuredPath = Path.of(configuredCli).toAbsolutePath().normalize(); @@ -318,6 +391,11 @@ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier */ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier, String version, AtomicPublisher publisher) throws IOException { + return extractRuntimeToCache(cacheBase, loader, classifier, version, publisher, true); + } + + private static Path extractRuntimeToCache(Path cacheBase, ClassLoader loader, String classifier, String version, + AtomicPublisher publisher, boolean extractCli) throws IOException { String resourcePath = "native/" + classifier + "/" + RUNTIME_FILENAME; String nativeVersion = readNativePackageVersion(loader, classifier); Path cacheDir = cacheBase.resolve(version).resolve(nativeVersion).resolve(classifier); @@ -325,7 +403,10 @@ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier // Step 1 — fast path: return an existing valid cache entry. if (isValidCachedFile(cached)) { - extractCliToCache(cacheDir, loader, classifier, publisher); + extractRuntimeAssetsToCache(cacheDir, loader, classifier, publisher); + if (extractCli) { + extractCliToCache(cacheDir, loader, classifier, publisher); + } return cached; } @@ -348,12 +429,68 @@ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier tryDelete(temp); } - // Step 5 — also extract the copilot CLI executable alongside runtime.node. - extractCliToCache(cacheDir, loader, classifier, publisher); + extractRuntimeAssetsToCache(cacheDir, loader, classifier, publisher); + if (extractCli) { + extractCliToCache(cacheDir, loader, classifier, publisher); + } return cached; } + private static void extractRuntimeAssetsToCache(Path cacheDir, ClassLoader loader, String classifier, + AtomicPublisher publisher) throws IOException { + String inventoryResourcePath = "native/" + classifier + "/" + RUNTIME_ASSETS_FILENAME; + URL inventoryResource = loader.getResource(inventoryResourcePath); + if (inventoryResource == null) { + return; + } + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(inventoryResource.openStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.isBlank()) { + continue; + } + String[] fields = line.split("\\t", 2); + if (fields.length != 2) { + throw new IOException("Invalid runtime asset inventory entry: " + line); + } + boolean executable = (Integer.parseInt(fields[0], 8) & 0111) != 0; + Path relative = Path.of(fields[1]).normalize(); + if (relative.isAbsolute() || relative.startsWith("..")) { + throw new IOException("Unsafe runtime asset inventory path: " + fields[1]); + } + Path cached = cacheDir.resolve(relative).normalize(); + if (!cached.startsWith(cacheDir)) { + throw new IOException("Runtime asset escapes cache directory: " + fields[1]); + } + if (isValidCachedFile(cached) && (!executable || isWindows() || Files.isExecutable(cached))) { + continue; + } + + String resourcePath = "native/" + classifier + "/" + fields[1]; + URL resource = loader.getResource(resourcePath); + if (resource == null) { + throw new FileNotFoundException("Runtime asset not found on classpath: " + resourcePath); + } + Files.createDirectories(cached.getParent()); + Path temp = Files.createTempFile(cached.getParent(), "runtime-asset-tmp-", ""); + try { + copyResourceToTemp(resource, resourcePath, temp); + if (executable) { + makeExecutable(temp); + } + publisher.publish(temp, cached); + } finally { + tryDelete(temp); + } + } + } catch (NumberFormatException ex) { + throw new IOException("Invalid runtime asset mode in " + inventoryResourcePath, ex); + } + } + /** * Extracts the copilot CLI executable from the classpath to the same cache * directory as {@code runtime.node}. Idempotent — skips extraction if already diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java index d3515509b5..a9c9cbcda7 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java @@ -192,19 +192,20 @@ public CopilotClientOptions setCliArgs(String[] cliArgs) { } /** - * Gets the path to the Copilot CLI executable. + * Gets the path to an explicitly configured Copilot executable. * - * @return the CLI path, or {@code null} to use "copilot" from PATH + * @return the executable path, or {@code null} to use the bundled runtime + * wrapper */ public String getCliPath() { return cliPath; } /** - * Sets the path to the Copilot CLI executable. + * Sets the path to the Copilot CLI or runtime wrapper executable. * * @param cliPath - * the path to the CLI executable + * the path to the executable * @return this options instance for method chaining */ public CopilotClientOptions setCliPath(String cliPath) { diff --git a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java index 68555a35b4..a227be04b9 100644 --- a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java @@ -9,9 +9,13 @@ import java.io.IOException; import java.net.ServerSocket; import java.net.URI; +import java.nio.file.Path; +import java.util.Map; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import com.github.copilot.ffi.NativeRuntimeLoader; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.TelemetryConfig; @@ -22,6 +26,47 @@ */ class CliServerManagerTest { + @TempDir + Path tempDir; + + @Test + void explicitCliPathDoesNotRequireRuntimeBundle() throws Exception { + Path explicit = tempDir.resolve("copilot"); + var manager = new CliServerManager(new CopilotClientOptions().setCliPath(explicit.toString())); + + assertEquals(explicit.toString(), manager.resolveCliLaunch().executable()); + } + + @Test + void inheritedCliPathEnvironmentOverrideDoesNotRequireRuntimeBundle() throws Exception { + Path inherited = tempDir.resolve("copilot-runtime"); + var manager = new CliServerManager(new CopilotClientOptions()); + + assertEquals(inherited.toString(), manager.resolveCliLaunch(inherited.toString()).executable()); + } + + @Test + void configuredEnvironmentCliPathOverridesInheritedEnvironment() throws Exception { + Path inherited = tempDir.resolve("inherited-copilot-runtime"); + Path configured = tempDir.resolve("configured-copilot-runtime"); + var options = new CopilotClientOptions() + .setEnvironment(Map.of(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV, configured.toString())); + var manager = new CliServerManager(options); + + assertEquals(configured.toString(), manager.resolveCliLaunch(inherited.toString()).executable()); + } + + @Test + void explicitCliPathOverridesEnvironment() throws Exception { + Path explicit = tempDir.resolve("explicit-copilot-runtime"); + Path configured = tempDir.resolve("configured-copilot-runtime"); + var options = new CopilotClientOptions().setCliPath(explicit.toString()) + .setEnvironment(Map.of(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV, configured.toString())); + var manager = new CliServerManager(options); + + assertEquals(explicit.toString(), manager.resolveCliLaunch("inherited-copilot-runtime").executable()); + } + // ===== parseCliUrl tests ===== @Test diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java index 23408faa2a..0294b511ec 100644 --- a/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java +++ b/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java @@ -50,10 +50,10 @@ * *

* Run with {@code mvn verify -Pinprocess} from the {@code java} reactor root, - * which builds the {@code copilot-sdk-java-runtime} artifact and sets - * {@code COPILOT_CLI_PATH} to the pinned CLI whose sibling {@code runtime.node} - * this test loads, and forces {@code forkCount=1} because the FFI host and env - * guard mutate process-global state. + * which builds the {@code copilot-sdk-java-runtime} artifact and sets the + * classifier JAR containing {@code runtime.node}, and forces + * {@code forkCount=1} because the FFI host and env guard mutate process-global + * state. * *

* {@link RequireInProcess} disables this test unless the {@code -Pinprocess} @@ -86,11 +86,6 @@ void shouldStartPingAndStopOverInProcessFfi() throws Exception { // replay proxy, mirroring how a session-level in-process test would // redirect COPILOT_API_URL. `ping` never reaches the network, but this // demonstrates the guard's intended usage for future in-process tests. - // COPILOT_CLI_PATH is intentionally NOT set here: NativeRuntimeLoader and - // CopilotClient.resolveInProcessEntrypoint() read it via - // System.getenv(), which is a JVM-startup-time snapshot that native - // setenv() calls made after the JVM starts cannot update — it must be - // set before the JVM starts (see the -Pinprocess Maven profile). try (InProcessEnvGuard envGuard = new InProcessEnvGuard(Map.of("COPILOT_API_URL", ctx.getProxyUrl()))) { CopilotClientOptions options = new CopilotClientOptions().setConnection(RuntimeConnection.forInProcess()); try (CopilotClient client = new CopilotClient(options)) { diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java new file mode 100644 index 0000000000..19c14a0644 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.e2e; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.AllowCopilotExperimental; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.CopilotClientOptions; +import com.github.copilot.rpc.PingResponse; +import com.github.copilot.rpc.RuntimeConnection; + +/** + * Failsafe smoke test for the managed out-of-process runtime wrapper. + */ +@AllowCopilotExperimental +@RequireInProcess +class OutOfProcessTransportIT { + + @Test + void shouldStartPingAndStopOverStdio() throws Exception { + CopilotClientOptions options = new CopilotClientOptions().setConnection(RuntimeConnection.forStdio()); + try (CopilotClient client = new CopilotClient(options)) { + client.start().get(); + + PingResponse pong = client.ping("wrapper message").get(); + assertEquals("pong: wrapper message", pong.message()); + assertNotNull(pong.timestamp()); + + client.stop().get(); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java index 9321618575..64eff21726 100644 --- a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java +++ b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java @@ -5,11 +5,9 @@ package com.github.copilot.e2e; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeFalse; import java.nio.file.Files; import java.nio.file.Path; @@ -37,6 +35,8 @@ class RewindIT { private static final String FILE_NAME = "rewind-sdk.txt"; + private static final String ORIGINAL_FILE_CONTENT = "Original rewind content"; + private static final String PREPARED_FILE_CONTENT = "Prepared rewind content"; private static final String FILE_CONTENT = "SDK rewind content"; private static E2ETestContext ctx; @@ -55,11 +55,9 @@ static void teardown() throws Exception { @Test void shouldRestoreTrackedFileAndConversation() throws Exception { - assumeFalse(System.getProperty("os.name").startsWith("Windows"), - "blocked on CLI 1.0.81 file-change tracking regression on Windows"); - ctx.configureForTest("rewind", "should_restore_tracked_file_and_conversation"); Path filePath = ctx.getWorkDir().resolve(FILE_NAME); + Files.writeString(filePath, ORIGINAL_FILE_CONTENT); try (CopilotClient client = ctx.createClient(); CopilotSession session = client @@ -67,11 +65,19 @@ void shouldRestoreTrackedFileAndConversation() throws Exception { new SessionConfig().setModel("claude-sonnet-4.5").setEnableFileChangeTracking(true) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) .get(30, TimeUnit.SECONDS)) { + AssistantMessageEvent ready = session + .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of " + + FILE_NAME + " from " + ORIGINAL_FILE_CONTENT + " to " + PREPARED_FILE_CONTENT + + ". After the tool succeeds, reply with exactly SDK_REWIND_READY."), 30_000) + .get(60, TimeUnit.SECONDS); + assertNotNull(ready); + assertEquals("SDK_REWIND_READY", ready.getData().content()); + assertEquals(PREPARED_FILE_CONTENT, Files.readString(filePath)); + AssistantMessageEvent response = session - .sendAndWait(new MessageOptions().setPrompt( - "Use the create tool to create " + FILE_NAME + " containing exactly " + FILE_CONTENT - + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE."), - 30_000) + .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of " + + FILE_NAME + " from " + PREPARED_FILE_CONTENT + " to " + FILE_CONTENT + + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE."), 30_000) .get(60, TimeUnit.SECONDS); assertNotNull(response); @@ -80,8 +86,9 @@ void shouldRestoreTrackedFileAndConversation() throws Exception { SessionHistoryListRewindPointsResult rewindPoints = waitForRewindPoints(session); assertTrue(Boolean.TRUE.equals(rewindPoints.fileChangeTrackingEnabled())); - assertEquals(1, rewindPoints.points().size()); - var rewindPoint = rewindPoints.points().get(0); + assertEquals(2, rewindPoints.points().size()); + var rewindPoint = rewindPoints.points().get(1); + assertTrue(Boolean.TRUE.equals(rewindPoint.turnChangedFiles())); assertTrue(Boolean.TRUE.equals(rewindPoint.canRestoreFiles())); assertEquals(1L, rewindPoint.fileCount()); @@ -98,7 +105,7 @@ void shouldRestoreTrackedFileAndConversation() throws Exception { assertTrue(rewind.eventsRemoved() != null && rewind.eventsRemoved() > 0); assertEquals(1, rewind.restoredFiles().size()); assertSamePath(filePath, rewind.restoredFiles().get(0)); - assertFalse(Files.exists(filePath)); + assertEquals(PREPARED_FILE_CONTENT, Files.readString(filePath)); var events = session.getMessages().get(10, TimeUnit.SECONDS); assertTrue(events.stream().noneMatch(event -> event.getId().toString().equals(rewindPoint.eventId()))); @@ -110,16 +117,19 @@ private static SessionHistoryListRewindPointsResult waitForRewindPoints(CopilotS SessionHistoryListRewindPointsResult result; do { result = session.getRpc().history.listRewindPoints().get(10, TimeUnit.SECONDS); - if (result.unavailableReason() == null && !result.points().isEmpty() - && Boolean.TRUE.equals(result.points().get(0).canRestoreFiles())) { + if (result.unavailableReason() == null && result.points().size() == 2 + && Boolean.TRUE.equals(result.points().get(1).turnChangedFiles()) + && Boolean.TRUE.equals(result.points().get(1).canRestoreFiles())) { return result; } TimeUnit.MILLISECONDS.sleep(100); } while (System.nanoTime() < deadline); assertNull(result.unavailableReason(), "Timed out waiting for rewind points to become available"); - assertFalse(result.points().isEmpty(), "Timed out waiting for a rewind point"); - assertTrue(Boolean.TRUE.equals(result.points().get(0).canRestoreFiles()), + assertTrue(result.points().size() >= 2, "Timed out waiting for both rewind points"); + assertTrue(Boolean.TRUE.equals(result.points().get(1).turnChangedFiles()), + "Timed out waiting for the edit turn to capture file changes"); + assertTrue(Boolean.TRUE.equals(result.points().get(1).canRestoreFiles()), "Timed out waiting for rewind file restoration to become available"); return result; } diff --git a/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java b/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java index cc98d24f6b..545b316c8c 100644 --- a/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java @@ -99,6 +99,47 @@ public boolean connectionClose(int connectionId) { assertEquals("1", env.get("COPILOT_DISABLE_KEYTAR")); } + @Test + void startWithoutEntrypointPassesOnlyRuntimeOptions() throws Exception { + AtomicReference argvJson = new AtomicReference<>(); + NativeBinding binding = new NativeBinding() { + @Override + public int hostStart(byte[] argv, int argvLen, byte[] env, int envLen) { + argvJson.set(argv); + return 11; + } + + @Override + public boolean hostShutdown(int serverId) { + return true; + } + + @Override + public int connectionOpen(int serverId, OutboundCallback callback, Pointer userData, byte[] extSource, + int extSourceLen, byte[] extName, int extNameLen, byte[] connToken, int connTokenLen) { + return 21; + } + + @Override + public boolean connectionWrite(int connectionId, byte[] data, int dataLen) { + return true; + } + + @Override + public boolean connectionClose(int connectionId) { + return true; + } + }; + + try (FfiRuntimeHost host = new FfiRuntimeHost(binding, "/tmp/runtime.node")) { + host.start(null, new CopilotClientOptions().setLogLevel("debug")); + } + + List argv = MAPPER.readValue(argvJson.get(), new TypeReference>() { + }); + assertEquals(List.of("--log-level", "debug"), argv); + } + @Test void callbackExceptionIsContainedAndDoesNotEscapeAcrossFfiBoundary() { AtomicBoolean callbackReturned = new AtomicBoolean(false); diff --git a/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java b/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java index fc8bd51011..84ef7d8de4 100644 --- a/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java @@ -34,17 +34,13 @@ class NativeRuntimeLoaderTest { - private static final String TEST_CLASSIFIER = PlatformDetector.detectClassifier(); - private static final String OTHER_CLASSIFIER = TEST_CLASSIFIER.equals("darwin-arm64") - ? "linux-x64" - : "darwin-arm64"; - private static final String TEST_CLI_FILENAME = TEST_CLASSIFIER.startsWith("win32") - ? NativeRuntimeLoader.CLI_FILENAME_WINDOWS - : NativeRuntimeLoader.CLI_FILENAME; + private static final String TEST_CLASSIFIER = "linux-x64"; + private static final String OTHER_CLASSIFIER = "darwin-arm64"; private static final String TEST_VERSION = "1.2.3-test"; private static final String TEST_NATIVE_VERSION = "0.0.1-test"; private static final byte[] FAKE_BINARY_CONTENT = "fake runtime.node binary content".getBytes(); private static final byte[] FAKE_CLI_CONTENT = "fake copilot CLI content".getBytes(); + private static final byte[] FAKE_WRAPPER_CONTENT = "fake runtime wrapper content".getBytes(); private static final byte[] OTHER_BINARY_CONTENT = "other runtime.node binary content".getBytes(); private static final byte[] OTHER_CLI_CONTENT = "other copilot CLI content".getBytes(); @@ -160,19 +156,24 @@ void resolveEntrypointUsesConfiguredCliWhenRuntimeIsInPrebuilds(@TempDir Path te @Test void resolveFromCliPathReturnsAbsolutePathForRelativeCliPath() throws Exception { Path workingDirectory = Path.of("").toAbsolutePath(); - Path fakeCliDir = Files.createTempDirectory(Path.of("target").toAbsolutePath(), "relative-cli-test-"); - Path fakeCliPath = fakeCliDir.resolve("copilot"); - Files.createFile(fakeCliPath); - Path runtimeNode = fakeCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME); - Files.write(runtimeNode, FAKE_BINARY_CONTENT); - - Path relativeCliPath = workingDirectory.relativize(fakeCliPath); - - assertEquals(runtimeNode, NativeRuntimeLoader.resolveFromCliPath(relativeCliPath.toString())); + Path fakeCliDir = Files.createTempDirectory(workingDirectory.resolve("target"), "relative-cli-"); + try { + Path fakeCliPath = Files.createFile(fakeCliDir.resolve("copilot")); + Path runtimeNode = Files.write(fakeCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME), + FAKE_BINARY_CONTENT); + Path relativeCliPath = workingDirectory.relativize(fakeCliPath); + + assertEquals(runtimeNode, NativeRuntimeLoader.resolveFromCliPath(relativeCliPath.toString())); + } finally { + Files.deleteIfExists(fakeCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME)); + Files.deleteIfExists(fakeCliDir.resolve("copilot")); + Files.deleteIfExists(fakeCliDir); + } } @Test void cliPathOverrideTakesPriorityOverClasspathExtraction(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); // Create a valid runtime.node alongside the fake CLI path Path fakeCliDir = tempDir.resolve("cli-dir"); Files.createDirectories(fakeCliDir); @@ -197,6 +198,7 @@ void cliPathOverrideTakesPriorityOverClasspathExtraction(@TempDir Path tempDir) @Test void extractToCacheCopiesResourceToVersionedCacheDirectory(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -211,6 +213,7 @@ void extractToCacheCopiesResourceToVersionedCacheDirectory(@TempDir Path tempDir @Test void extractToCacheReturnsCachedFileOnSecondCall(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -230,6 +233,7 @@ void extractToCacheReturnsCachedFileOnSecondCall(@TempDir Path tempDir) throws E @Test void changedNativeVersionDoesNotReuseCachedArtifactsForSameSdkVersion(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader firstLoader = classLoaderWithNativeArtifacts(tempDir.resolve("native-v1"), TEST_CLASSIFIER, "1.0.0", FAKE_BINARY_CONTENT, FAKE_CLI_CONTENT); @@ -241,13 +245,16 @@ void changedNativeVersionDoesNotReuseCachedArtifactsForSameSdkVersion(@TempDir P assertNotEquals(firstRuntime, secondRuntime, "Different native versions must use different cache entries"); assertBytesEqual(FAKE_BINARY_CONTENT, Files.readAllBytes(firstRuntime)); - assertBytesEqual(FAKE_CLI_CONTENT, Files.readAllBytes(firstRuntime.getParent().resolve(TEST_CLI_FILENAME))); + assertBytesEqual(FAKE_CLI_CONTENT, + Files.readAllBytes(firstRuntime.getParent().resolve(NativeRuntimeLoader.CLI_FILENAME))); assertBytesEqual(OTHER_BINARY_CONTENT, Files.readAllBytes(secondRuntime)); - assertBytesEqual(OTHER_CLI_CONTENT, Files.readAllBytes(secondRuntime.getParent().resolve(TEST_CLI_FILENAME))); + assertBytesEqual(OTHER_CLI_CONTENT, + Files.readAllBytes(secondRuntime.getParent().resolve(NativeRuntimeLoader.CLI_FILENAME))); } @Test void extractToCacheThrowsWhenClasspathResourceMissing(@TempDir Path tempDir) { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader emptyLoader = new URLClassLoader(new URL[0], null); @@ -257,6 +264,7 @@ void extractToCacheThrowsWhenClasspathResourceMissing(@TempDir Path tempDir) { @Test void extractToCacheThrowsWhenNativeMetadataMissing(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path resourceDir = tempDir.resolve("native").resolve(TEST_CLASSIFIER); Files.createDirectories(resourceDir); Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME), FAKE_BINARY_CONTENT); @@ -270,6 +278,7 @@ void extractToCacheThrowsWhenNativeMetadataMissing(@TempDir Path tempDir) throws @Test void extractedBinaryContentsMatchClasspathResource(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -281,6 +290,7 @@ void extractedBinaryContentsMatchClasspathResource(@TempDir Path tempDir) throws @Test void extractToCacheFiltersClasspathByClassifier(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); writeRuntimeResource(tempDir, TEST_CLASSIFIER, FAKE_BINARY_CONTENT); writeRuntimeResource(tempDir, OTHER_CLASSIFIER, OTHER_BINARY_CONTENT); @@ -294,6 +304,7 @@ void extractToCacheFiltersClasspathByClassifier(@TempDir Path tempDir) throws Ex @Test void extractToCacheRepairsInvalidCacheEntry(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); Path cached = cacheBase.resolve(TEST_VERSION).resolve(TEST_NATIVE_VERSION).resolve(TEST_CLASSIFIER) .resolve(NativeRuntimeLoader.RUNTIME_FILENAME); @@ -309,13 +320,13 @@ void extractToCacheRepairsInvalidCacheEntry(@TempDir Path tempDir) throws Except @Test void nonExecutableCachedCliIsNotAcceptedAsValid(@TempDir Path tempDir) throws Exception { - assumeTrue(!TEST_CLASSIFIER.startsWith("win32")); + assumeLinuxX64(); assumeTrue(Files.getFileStore(tempDir).supportsFileAttributeView("posix")); Path cacheBase = tempDir.resolve("cache"); Path cacheDir = cacheBase.resolve(TEST_VERSION).resolve(TEST_NATIVE_VERSION).resolve(TEST_CLASSIFIER); Files.createDirectories(cacheDir); Files.write(cacheDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME), FAKE_BINARY_CONTENT); - Path cachedCli = Files.write(cacheDir.resolve(TEST_CLI_FILENAME), FAKE_CLI_CONTENT); + Path cachedCli = Files.write(cacheDir.resolve(NativeRuntimeLoader.CLI_FILENAME), FAKE_CLI_CONTENT); Files.setPosixFilePermissions(cachedCli, PosixFilePermissions.fromString("rw-------")); ClassLoader loader = classLoaderWithRuntimeAndCliResources(tempDir, TEST_CLASSIFIER); @@ -330,6 +341,7 @@ void nonExecutableCachedCliIsNotAcceptedAsValid(@TempDir Path tempDir) throws Ex @Test void bundledCliSiblingIsUsedWhenClasspathResourceAbsent(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path bundledCliDir = tempDir.resolve("bundled-cli"); Files.createDirectories(bundledCliDir); Path runtimeNode = bundledCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME); @@ -347,6 +359,7 @@ void bundledCliSiblingIsUsedWhenClasspathResourceAbsent(@TempDir Path tempDir) t @Test void classpathResourceWinsOverBundledCliSibling(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); // Source 3: bundled CLI dir with runtime.node (should NOT win) Path bundledCliDir = tempDir.resolve("bundled-cli"); Files.createDirectories(bundledCliDir); @@ -368,6 +381,7 @@ void classpathResourceWinsOverBundledCliSibling(@TempDir Path tempDir) throws Ex @Test void bundledCliSiblingIsIgnoredWhenRuntimeNodeMissing(@TempDir Path tempDir) { + assumeLinuxX64(); Path bundledCliDir = tempDir.resolve("bundled-cli-no-runtime"); // bundledCliDir doesn't even exist — no runtime.node present @@ -399,12 +413,12 @@ void defaultPublisherMovesSourceToTarget(@TempDir Path tempDir) throws Exception @Test void cliIsExecutableBeforeAtomicPublication(@TempDir Path tempDir) throws Exception { - assumeTrue(!TEST_CLASSIFIER.startsWith("win32")); + assumeLinuxX64(); assumeTrue(Files.getFileStore(tempDir).supportsFileAttributeView("posix")); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeAndCliResources(tempDir, TEST_CLASSIFIER); NativeRuntimeLoader.AtomicPublisher publisher = (temp, cached) -> { - if (cached.getFileName().toString().equals(TEST_CLI_FILENAME)) { + if (cached.getFileName().toString().equals(NativeRuntimeLoader.CLI_FILENAME)) { assertTrue(Files.isExecutable(temp), "CLI temp file must be executable before atomic publication"); } Files.move(temp, cached, StandardCopyOption.REPLACE_EXISTING); @@ -415,6 +429,7 @@ void cliIsExecutableBeforeAtomicPublication(@TempDir Path tempDir) throws Except @Test void extractionCleansUpTempFileWhenPublicationFails(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -435,6 +450,7 @@ void extractionCleansUpTempFileWhenPublicationFails(@TempDir Path tempDir) throw @Test void extractionCleansUpTempFileWhenPublisherThrowsIllegalStateException(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -462,6 +478,7 @@ void extractionCleansUpTempFileWhenPublisherThrowsIllegalStateException(@TempDir @Test void concurrentExtractionByMultipleThreadsBothSucceed(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); int threadCount = 8; @@ -499,6 +516,7 @@ void concurrentExtractionByMultipleThreadsBothSucceed(@TempDir Path tempDir) thr @Test void resolveWithNullCliEnvExtractsFromClasspath(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -509,8 +527,54 @@ void resolveWithNullCliEnvExtractsFromClasspath(@TempDir Path tempDir) throws Ex assertTrue(Files.size(result) > 0); } + @Test + void resolveRuntimeWrapperExtractsAdjacentPairFromAbsentCache(@TempDir Path tempDir) throws Exception { + Path cacheBase = tempDir.resolve("cache"); + assertFalse(Files.exists(cacheBase)); + ClassLoader loader = classLoaderWithRuntimeWrapperArtifacts(tempDir, TEST_CLASSIFIER, TEST_NATIVE_VERSION); + + Path wrapper = NativeRuntimeLoader.resolveRuntimeWrapper(cacheBase, loader, TEST_CLASSIFIER, TEST_VERSION); + + assertEquals(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME, wrapper.getFileName().toString()); + assertTrue(Files.isRegularFile(wrapper)); + assertTrue(Files.isRegularFile(wrapper.resolveSibling(NativeRuntimeLoader.RUNTIME_FILENAME))); + assertFalse(Files.exists(wrapper.resolveSibling(NativeRuntimeLoader.CLI_FILENAME))); + } + + @Test + void resolveRuntimeWrapperExtractsRetainedRuntimeAssets(@TempDir Path tempDir) throws Exception { + Path resourceDir = tempDir.resolve("native").resolve(TEST_CLASSIFIER); + writeRuntimeResource(tempDir, TEST_CLASSIFIER, FAKE_BINARY_CONTENT); + Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME), FAKE_WRAPPER_CONTENT); + Path ripgrep = resourceDir.resolve("ripgrep/bin/linux-x64/rg"); + Files.createDirectories(ripgrep.getParent()); + Files.writeString(ripgrep, "ripgrep"); + Files.writeString(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_ASSETS_FILENAME), + "644\truntime.node\n" + "755\tcopilot-runtime\n" + "755\tripgrep/bin/linux-x64/rg\n"); + ClassLoader loader = new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); + + Path wrapper = NativeRuntimeLoader.resolveRuntimeWrapper(tempDir.resolve("cache"), loader, TEST_CLASSIFIER, + TEST_VERSION); + + Path installedRipgrep = wrapper.getParent().resolve("ripgrep/bin/linux-x64/rg"); + assertEquals("ripgrep", Files.readString(installedRipgrep)); + assertTrue(Files.isExecutable(installedRipgrep)); + } + + @Test + void resolveRuntimeWrapperRejectsClassifierWithoutWrapper(@TempDir Path tempDir) throws Exception { + writeRuntimeResource(tempDir, TEST_CLASSIFIER, FAKE_BINARY_CONTENT); + ClassLoader loader = new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); + + IOException error = assertThrows(IOException.class, () -> NativeRuntimeLoader + .resolveRuntimeWrapper(tempDir.resolve("cache"), loader, TEST_CLASSIFIER, TEST_VERSION)); + + assertTrue(error.getMessage().contains(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME)); + } + @Test void resolveThrowsWhenNoSourceIsAvailable(@TempDir Path tempDir) { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader emptyLoader = new URLClassLoader(new URL[0], null); @@ -521,6 +585,7 @@ void resolveThrowsWhenNoSourceIsAvailable(@TempDir Path tempDir) { @Test void resolveFallsBackToRuntimeAlongsideBundledCli(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader emptyLoader = new URLClassLoader(new URL[0], null); Path bundledCli = tempDir.resolve("copilot"); @@ -538,6 +603,17 @@ void resolveFallsBackToRuntimeAlongsideBundledCli(@TempDir Path tempDir) throws // Helpers // ------------------------------------------------------------------------- + private static void assumeLinuxX64() { + String actualClassifier; + try { + actualClassifier = PlatformDetector.detectClassifier(); + } catch (IllegalStateException ex) { + actualClassifier = "unsupported"; + } + assumeTrue(TEST_CLASSIFIER.equals(actualClassifier), + "Requires linux-x64; detected " + actualClassifier + "; see #2323"); + } + private static ClassLoader classLoaderWithVersionResource(Path tempDir, String version) throws IOException { Path propsFile = tempDir.resolve(NativeRuntimeLoader.VERSION_RESOURCE); Files.writeString(propsFile, "version=" + version + "\n"); @@ -553,7 +629,7 @@ private static ClassLoader classLoaderWithRuntimeAndCliResources(Path tempDir, S throws IOException { writeRuntimeResource(tempDir, classifier, FAKE_BINARY_CONTENT); Path resourceDir = tempDir.resolve("native").resolve(classifier); - Files.write(resourceDir.resolve(TEST_CLI_FILENAME), FAKE_CLI_CONTENT); + Files.write(resourceDir.resolve(NativeRuntimeLoader.CLI_FILENAME), FAKE_CLI_CONTENT); return new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); } @@ -561,7 +637,19 @@ private static ClassLoader classLoaderWithNativeArtifacts(Path tempDir, String c byte[] runtimeContent, byte[] cliContent) throws IOException { writeRuntimeResource(tempDir, classifier, runtimeContent); Path resourceDir = tempDir.resolve("native").resolve(classifier); - Files.write(resourceDir.resolve(TEST_CLI_FILENAME), cliContent); + Files.write(resourceDir.resolve(NativeRuntimeLoader.CLI_FILENAME), cliContent); + Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME), FAKE_WRAPPER_CONTENT); + Files.writeString(resourceDir.resolve("platform.properties"), + "classifier=" + classifier + "\nversion=" + nativeVersion + "\n"); + return new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); + } + + private static ClassLoader classLoaderWithRuntimeWrapperArtifacts(Path tempDir, String classifier, + String nativeVersion) throws IOException { + writeRuntimeResource(tempDir, classifier, FAKE_BINARY_CONTENT); + Path resourceDir = tempDir.resolve("native").resolve(classifier); + Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME), FAKE_WRAPPER_CONTENT); + Files.write(resourceDir.resolve(NativeRuntimeLoader.CLI_FILENAME), FAKE_CLI_CONTENT); Files.writeString(resourceDir.resolve("platform.properties"), "classifier=" + classifier + "\nversion=" + nativeVersion + "\n"); return new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); diff --git a/nodejs/README.md b/nodejs/README.md index 53e81c7aa4..5886df234f 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -95,6 +95,7 @@ new CopilotClient(options?: CopilotClientOptions) - `RuntimeConnection.forUri(url, { connectionToken? })` — connect to an already-running runtime (mutually exclusive with `gitHubToken`/`useLoggedInUser`). There is no top-level `cliUrl` shortcut; use this factory for URL-based connections. - `RuntimeConnection.forInProcess()` — host the runtime in-process over its native C ABI (FFI). **Experimental.** Because the runtime shares this process, `env`, `telemetry`, and `workingDirectory` are rejected with this transport; set them on the host process instead. - The child-process transports (`forStdio`/`forTcp`) also accept a per-connection `env`. Set it there or via the top-level `env` option — not both (setting both throws). + - Managed child-process connections materialize the bundled `copilot-runtime` and adjacent `runtime.node`, then launch the wrapper by default. An explicit connection `path` or `COPILOT_CLI_PATH` overrides the bundled runtime. - `mode?: "empty" | "copilot-cli"` - Defaulting strategy. Use `"empty"` for multi-user server mode; defaults to `"copilot-cli"`. - `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd). - `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`. diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 13bb9c904e..4f460e9577 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -16,7 +16,7 @@ import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; import { createRequire } from "node:module"; import { Socket } from "node:net"; -import { dirname, isAbsolute, join } from "node:path"; +import { dirname, isAbsolute, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { createMessageConnection, @@ -43,6 +43,7 @@ import type { import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession } from "./session.js"; import type { FfiRuntimeHost } from "./ffiRuntimeHost.js"; +import { materializeRuntimeBundle } from "./runtimeArtifacts.js"; import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js"; import { createCopilotRequestAdapter } from "./copilotRequestHandler.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; @@ -365,16 +366,19 @@ function getCliPlatformPackageNames(): string[] { return variants.map((variant) => `@github/copilot-${variant}-${arch}`); } +interface BundledCliPackage { + root: string; + platform: string; +} + /** - * Gets the path to the bundled CLI from the platform-specific @github/copilot-* - * package. Uses index.js directly rather than the native binary so the CLI runs - * under the current Node.js runtime. + * Resolves the current platform package and its npm prebuilds folder. * * In ESM, uses import.meta.resolve directly. In CJS (e.g., VS Code extensions * bundled with esbuild format:"cjs"), import.meta is empty so we fall back to * walking node_modules to find the package. */ -function getBundledCliPath(): string { +function getBundledCliPackage(): BundledCliPackage { const packageNames = getCliPlatformPackageNames(); if (typeof import.meta.resolve === "function") { @@ -383,7 +387,10 @@ function getBundledCliPath(): string { try { const packageEntryUrl = import.meta.resolve(packageName); const packageEntryPath = fileURLToPath(packageEntryUrl); - return join(dirname(packageEntryPath), "index.js"); + return { + root: dirname(packageEntryPath), + platform: packageName.slice("@github/copilot-".length), + }; } catch { // Try the next candidate platform package. } @@ -400,9 +407,13 @@ function getBundledCliPath(): string { const searchPaths = req.resolve.paths("@github/copilot") ?? []; for (const base of searchPaths) { for (const packageName of packageNames) { - const candidate = join(base, ...packageName.split("/"), "index.js"); + const root = join(base, ...packageName.split("/")); + const candidate = join(root, "index.js"); if (existsSync(candidate)) { - return candidate; + return { + root, + platform: packageName.slice("@github/copilot-".length), + }; } } } @@ -413,6 +424,14 @@ function getBundledCliPath(): string { ); } +function getBundledRuntimePath(): string { + const bundled = getBundledCliPackage(); + return materializeRuntimeBundle({ + packageRoot: bundled.root, + platform: bundled.platform, + }); +} + /** * Main client for interacting with the Copilot CLI. * @@ -520,6 +539,7 @@ export class CopilotClient { private _rpc: ReturnType | null = null; private _internalRpc: ReturnType | null = null; private processExitPromise: Promise | null = null; // Rejects when CLI process exits + private processTransportError: Error | null = null; private negotiatedProtocolVersion: number | null = null; /** Connection-level session filesystem config, set via constructor option. */ private sessionFsConfig: SessionFsConfig | null = null; @@ -733,10 +753,14 @@ export class CopilotClient { conn.kind === "stdio" || conn.kind === "tcp" ? conn.env : undefined; const effectiveEnv = connEnv ?? options.env ?? process.env; this.resolvedEnv = effectiveEnv; - this.resolvedCliPath = - conn.kind === "stdio" || conn.kind === "tcp" - ? (conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath()) - : undefined; + if (conn.kind === "stdio" || conn.kind === "tcp") { + const explicitCliPath = conn.path ?? effectiveEnv.COPILOT_CLI_PATH; + if (explicitCliPath) { + this.resolvedCliPath = explicitCliPath; + } else { + this.resolvedCliPath = getBundledRuntimePath(); + } + } // Collect extra CLI args from the connection variant (if any). const connArgs: readonly string[] = @@ -951,6 +975,8 @@ export class CopilotClient { return; } + this.forceStopping = false; + this.processTransportError = null; this.state = "connecting"; try { @@ -997,8 +1023,10 @@ export class CopilotClient { this.state = "connected"; } catch (error) { + const startupError = this.processTransportError ?? error; + await this.forceStop(); this.state = "error"; - throw error; + throw startupError; } } @@ -2717,27 +2745,27 @@ export class CopilotClient { // Set up a promise that rejects when the process exits (used to race against RPC calls) this.processExitPromise = new Promise((_, rejectProcessExit) => { this.cliProcess!.on("exit", (code) => { - // Give a small delay for stderr to be fully captured - setTimeout(() => { - const stderrOutput = this.stderrBuffer.trim(); - if (stderrOutput) { - rejectProcessExit( - new Error( - `CLI server exited with code ${code}\nstderr: ${stderrOutput}` - ) - ); - } else { - rejectProcessExit( - new Error(`CLI server exited unexpectedly with code ${code}`) - ); - } - }, 50); + if (this.messageWriter) { + this.messageWriter.suppressWriteErrors = true; + } + const stderrOutput = this.stderrBuffer.trim(); + if (stderrOutput) { + rejectProcessExit( + new Error( + `CLI server exited with code ${code}\nstderr: ${stderrOutput}` + ) + ); + } else { + rejectProcessExit( + new Error(`CLI server exited unexpectedly with code ${code}`) + ); + } }); }); // Prevent unhandled rejection when process exits normally (we only use this in Promise.race) this.processExitPromise.catch(() => {}); - this.cliProcess.on("exit", (code) => { + this.cliProcess.on("close", (code) => { if (!resolved) { resolved = true; const stderrOutput = this.stderrBuffer.trim(); @@ -2782,7 +2810,15 @@ export class CopilotClient { /** Starts the in-process FFI runtime with SDK-managed typed options. */ private async startInProcessFfi(): Promise { - const entrypoint = this.resolveCliPathForFfi(); + const explicitEntrypoint = this.resolvedEnv.COPILOT_CLI_PATH; + const runtimeLibrary = explicitEntrypoint + ? join( + dirname(resolve(explicitEntrypoint)), + "prebuilds", + CopilotClient.getNapiPrebuildsFolder(explicitEntrypoint), + "runtime.node" + ) + : join(dirname(getBundledRuntimePath()), "runtime.node"); // Load the FFI host lazily so the native `koffi` addon (and its // platform-specific `koffi.node`) is only loaded on the in-process path; // out-of-process (stdio/tcp) consumers never touch the native dependency. @@ -2817,12 +2853,7 @@ export class CopilotClient { args.push("--remote"); } - const host = FfiRuntimeHost.create( - entrypoint, - CopilotClient.getNapiPrebuildsFolder(entrypoint), - environment, - args - ); + const host = FfiRuntimeHost.create(runtimeLibrary, explicitEntrypoint, environment, args); this.ffiHost = host; await host.start(); } @@ -2845,20 +2876,6 @@ export class CopilotClient { this.connection.listen(); } - /** - * Resolves the CLI entrypoint used for in-process FFI hosting: `COPILOT_CLI_PATH` - * when set, otherwise the bundled platform-package entrypoint. - */ - private resolveCliPathForFfi(): string { - return this.resolvedEnv.COPILOT_CLI_PATH ?? getBundledCliPath(); - } - - /** - * Returns the napi prebuilds folder name for the current host — the - * `-` convention (e.g. `win32-x64`, `darwin-arm64`, - * `linux-x64`, `linuxmusl-x64`) under which the runtime ships - * `prebuilds//runtime.node`. - */ private static getNapiPrebuildsFolder(entrypoint: string): string { const arch = process.arch; if (arch !== "x64" && arch !== "arm64") { @@ -2902,6 +2919,10 @@ export class CopilotClient { } this.state = "error"; const reason = err instanceof Error ? (err.stack ?? err.message) : String(err); + const stderrOutput = this.stderrBuffer.trim(); + this.processTransportError = new Error( + `CLI server connection failed: ${reason}${stderrOutput ? `\nstderr: ${stderrOutput}` : ""}` + ); this.logDebug(`stdin pipe error: ${reason}`); try { this.connection?.dispose(); diff --git a/nodejs/src/ffiRuntimeHost.ts b/nodejs/src/ffiRuntimeHost.ts index a92aa1589a..4795e325ce 100644 --- a/nodejs/src/ffiRuntimeHost.ts +++ b/nodejs/src/ffiRuntimeHost.ts @@ -7,10 +7,8 @@ * and speaking JSON-RPC over its C ABI (FFI) instead of spawning a CLI child process * and communicating over stdio/TCP. * - * The native `host_start` export spawns the CLI worker itself - * (`node --embedded-host` for a `.js` entrypoint, or ` - * --embedded-host` for a packaged binary), so the SDK never launches the worker - * directly. LSP `Content-Length:`-framed JSON-RPC bytes are pumped across the ABI: + * The native `host_start` export constructs the Rust server synchronously in this + * process. LSP `Content-Length:`-framed JSON-RPC bytes are pumped across the ABI: * writes go to `connection_write`; inbound frames arrive on a native callback that * feeds {@link FfiRuntimeHost.receiveStream}. The existing `vscode-jsonrpc` * `StreamMessageReader`/`StreamMessageWriter` handle framing unchanged — this is a @@ -19,7 +17,7 @@ import { existsSync } from "node:fs"; import koffi from "koffi"; -import { dirname, join, resolve } from "node:path"; +import { resolve } from "node:path"; import { PassThrough, Writable } from "node:stream"; const SYMBOL_PREFIX = "copilot_runtime_"; @@ -97,14 +95,12 @@ function loadLibrary(libraryPath: string): FfiLibrary { return loadedLibrary; } -function buildArgvJson(cliEntrypoint: string, args: readonly string[]): Buffer { - // A `.js` entrypoint is launched via node; the packaged single-file CLI binary - // embeds its own Node and is invoked directly. `--no-auto-update` pins the worker - // to the bundled pkg matching the loaded cdylib, instead of drifting to a newer - // version installed under the user's `~/.copilot/pkg` (which would cause ABI skew). - const argv = cliEntrypoint.toLowerCase().endsWith(".js") - ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"] - : [cliEntrypoint, "--embedded-host", "--no-auto-update"]; +function buildArgvJson(cliEntrypoint: string | undefined, args: readonly string[]): Buffer { + const argv = cliEntrypoint + ? cliEntrypoint.toLowerCase().endsWith(".js") + ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"] + : [cliEntrypoint, "--embedded-host", "--no-auto-update"] + : []; argv.push(...args); return Buffer.from(JSON.stringify(argv), "utf8"); } @@ -140,7 +136,7 @@ export class FfiRuntimeHost { private constructor( private readonly libraryPath: string, - private readonly cliEntrypoint: string, + private readonly cliEntrypoint: string | undefined, private readonly environment: Record | undefined, private readonly args: readonly string[] ) { @@ -161,41 +157,38 @@ export class FfiRuntimeHost { } /** - * Resolves the cdylib next to the given CLI entrypoint and prepares the FFI host. - * The cdylib is resolved as `prebuilds//runtime.node` relative to - * the entrypoint directory (the napi-rs `-` layout, e.g. - * `linux-x64`). Throws if it cannot be found. + * Loads the runtime cdylib at the given path and prepares the FFI host. */ static create( - cliEntrypoint: string, - prebuildsFolder: string, + libraryPath: string, + cliEntrypoint: string | undefined, environment: Record | undefined, args: readonly string[] ): FfiRuntimeHost { - const fullEntrypoint = resolve(cliEntrypoint); - const distDir = dirname(fullEntrypoint); - const libraryPath = join(distDir, "prebuilds", prebuildsFolder, "runtime.node"); - if (!existsSync(libraryPath)) { - throw new Error(`FFI runtime library not found. Looked for '${libraryPath}'.`); + const fullLibraryPath = resolve(libraryPath); + if (!existsSync(fullLibraryPath)) { + throw new Error(`FFI runtime library not found at '${fullLibraryPath}'.`); } - return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args); + return new FfiRuntimeHost( + fullLibraryPath, + cliEntrypoint ? resolve(cliEntrypoint) : undefined, + environment, + args + ); } - /** - * Starts the in-process runtime: spawns the CLI worker via the native host, - * waits for readiness, and opens the FFI JSON-RPC connection. - */ + /** Starts the in-process Rust runtime and opens the FFI JSON-RPC connection. */ async start(): Promise { const argvJson = buildArgvJson(this.cliEntrypoint, this.args); const envJson = buildEnvJson(this.environment); - // The native host spawns the CLI worker itself and has no cwd parameter, so the - // worker inherits this process's cwd. A custom working directory is intentionally + // The native host has no cwd parameter, so it uses this process's cwd. A custom + // working directory is intentionally // unsupported for the in-process transport (rejected by the client constructor) // rather than mutating the shared process-global cwd here. - // host_start blocks until the worker connects back and signals readiness - // (up to ~30s); run it as an async FFI call so the Node event loop isn't blocked. + // host_start constructs the native engine synchronously; run it as an async FFI + // call so the Node event loop isn't blocked. this.serverId = await new Promise((resolvePromise, rejectPromise) => { this.lib.hostStart.async( argvJson, @@ -212,9 +205,7 @@ export class FfiRuntimeHost { ); }); if (!this.serverId) { - throw new Error( - `copilot_runtime_host_start failed (library '${this.libraryPath}', entrypoint '${this.cliEntrypoint}').` - ); + throw new Error(`copilot_runtime_host_start failed (library '${this.libraryPath}').`); } this.outboundCallback = koffi.register( diff --git a/nodejs/src/runtimeArtifacts.ts b/nodejs/src/runtimeArtifacts.ts new file mode 100644 index 0000000000..19d9926250 --- /dev/null +++ b/nodejs/src/runtimeArtifacts.ts @@ -0,0 +1,183 @@ +import { createHash } from "node:crypto"; +import { + chmodSync, + copyFileSync, + existsSync, + lstatSync, + mkdirSync, + mkdtempSync, + readdirSync, + renameSync, + rmSync, + statSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join, relative, sep } from "node:path"; + +export interface RuntimeArtifactSources { + packageRoot: string; + platform: string; +} + +const EXCLUDED_TOP_LEVEL = new Set([ + "app.js", + "assets", + "changelog.json", + "copilot", + "copilot.exe", + "copilot-sdk", + "foundry-local-sdk", + "index.js", + "LICENSE.md", + "napi-oop-runtime", + "npm-loader.js", + "package.json", + "preloads", + "pvrecorder", + "queries", + "README.md", + "sdk", + "sea-loader.js", + "webview", +]); + +interface RuntimeAsset { + source: string; + relativePath: string; +} + +function validateFile(path: string, label: string): void { + if (!existsSync(path)) { + throw new Error(`${label} not found at ${path}.`); + } + if (statSync(path).size === 0) { + throw new Error(`${label} at ${path} is empty.`); + } +} + +function validateRuntimeBundle(wrapper: string, runtimeNode: string): void { + validateFile(wrapper, "Copilot runtime wrapper"); + validateFile(runtimeNode, "Copilot runtime.node"); +} + +function isExcluded(relativePath: string): boolean { + const parts = relativePath.split(sep); + const topLevel = parts[0]; + const fileName = parts.at(-1) ?? ""; + return ( + EXCLUDED_TOP_LEVEL.has(topLevel) || + /^tree-sitter.*\.wasm$/.test(topLevel) || + /^voice-.*\.js$/.test(topLevel) || + fileName === "cli-native.node" || + parts.includes("mediaremote-adapter") || + fileName.startsWith("copilot-runtime-bin") + ); +} + +function collectRuntimeAssets(sources: RuntimeArtifactSources): RuntimeAsset[] { + const assets: RuntimeAsset[] = []; + const visit = (directory: string): void => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const source = join(directory, entry.name); + const sourceRelative = relative(sources.packageRoot, source); + if (isExcluded(sourceRelative)) { + continue; + } + if (entry.isDirectory()) { + visit(source); + continue; + } + if (!entry.isFile() && !entry.isSymbolicLink()) { + continue; + } + + const parts = sourceRelative.split(sep); + let relativePath = sourceRelative; + if (parts[0] === "prebuilds") { + if (parts[1] !== sources.platform || parts.length < 3) { + continue; + } + relativePath = parts.slice(2).join(sep); + } + assets.push({ source, relativePath }); + } + }; + visit(sources.packageRoot); + return assets.sort((left, right) => left.relativePath.localeCompare(right.relativePath)); +} + +function sourceFingerprint(assets: RuntimeAsset[]): string { + const hash = createHash("sha256"); + for (const asset of assets) { + const stat = lstatSync(asset.source); + hash.update(asset.relativePath).update("\0"); + hash.update(`${stat.size}:${stat.mtimeMs}`).update("\0"); + } + return hash.digest("hex").slice(0, 20); +} + +function makeExecutable(path: string): void { + if (process.platform === "win32") { + return; + } + const mode = statSync(path).mode; + if ((mode & 0o111) === 0) { + chmodSync(path, mode | 0o111); + } +} + +export function defaultRuntimeCacheRoot( + platform = process.platform, + home = homedir(), + environment: NodeJS.ProcessEnv = process.env +): string { + const cacheDirectory = + platform === "win32" + ? (environment.LOCALAPPDATA ?? join(home, "AppData", "Local")) + : platform === "darwin" + ? join(home, "Library", "Caches") + : (environment.XDG_CACHE_HOME ?? join(home, ".cache")); + return join(cacheDirectory, "github-copilot-sdk", "runtime"); +} + +export function materializeRuntimeBundle( + sources: RuntimeArtifactSources, + cacheRoot = defaultRuntimeCacheRoot() +): string { + const assets = collectRuntimeAssets(sources); + const wrapperName = process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; + const sourceWrapper = assets.find((asset) => asset.relativePath === wrapperName)?.source; + const sourceRuntimeNode = assets.find((asset) => asset.relativePath === "runtime.node")?.source; + validateRuntimeBundle(sourceWrapper ?? "", sourceRuntimeNode ?? ""); + + const installDir = join(cacheRoot, `${sources.platform}-${sourceFingerprint(assets)}`); + const installedWrapper = join(installDir, wrapperName); + const installedRuntimeNode = join(installDir, "runtime.node"); + if (existsSync(installDir)) { + validateRuntimeBundle(installedWrapper, installedRuntimeNode); + makeExecutable(installedWrapper); + return installedWrapper; + } + + mkdirSync(cacheRoot, { recursive: true }); + const stagingDir = mkdtempSync(join(cacheRoot, ".runtime-")); + try { + for (const asset of assets) { + const destination = join(stagingDir, asset.relativePath); + mkdirSync(dirname(destination), { recursive: true }); + copyFileSync(asset.source, destination); + } + const stagedWrapper = join(stagingDir, wrapperName); + makeExecutable(stagedWrapper); + renameSync(stagingDir, installDir); + } catch (error) { + if (!existsSync(installDir)) { + throw error; + } + validateRuntimeBundle(installedWrapper, installedRuntimeNode); + } finally { + rmSync(stagingDir, { recursive: true, force: true }); + } + + return installedWrapper; +} diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 5baff82cb4..4017d00637 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -60,6 +60,28 @@ describe("approveAll", () => { }); describe("CopilotClient", () => { + it.each([ + { + source: "connection path", + connection: RuntimeConnection.forStdio({ path: "/explicit/copilot" }), + env: {}, + expected: "/explicit/copilot", + }, + { + source: "COPILOT_CLI_PATH", + connection: RuntimeConnection.forStdio(), + env: { COPILOT_CLI_PATH: "/environment/copilot" }, + expected: "/environment/copilot", + }, + ])( + "preserves explicit child-process override from $source", + ({ connection, env, expected }) => { + const client = new CopilotClient({ connection, env }); + + expect((client as any).resolvedCliPath).toBe(expected); + } + ); + async function startWithMockConnection( builtinPluginDirectories?: readonly string[] ): Promise> { diff --git a/nodejs/test/e2e/builtin_tools.e2e.test.ts b/nodejs/test/e2e/builtin_tools.e2e.test.ts index 36b70ea195..39900bc7d6 100644 --- a/nodejs/test/e2e/builtin_tools.e2e.test.ts +++ b/nodejs/test/e2e/builtin_tools.e2e.test.ts @@ -130,6 +130,19 @@ describe("Built-in Tools", async () => { async () => { await writeFile(join(workDir, "data.txt"), "apple\nbanana\napricot\ncherry\n"); const session = await client.createSession({ onPermissionRequest: approveAll }); + let grepToolCallId: string | undefined; + let grepCompletedSuccessfully = false; + session.on((event) => { + if (event.type === "tool.execution_start" && event.data.toolName === "grep") { + grepToolCallId = event.data.toolCallId; + } else if ( + event.type === "tool.execution_complete" && + event.data.toolCallId === grepToolCallId && + event.data.success + ) { + grepCompletedSuccessfully = true; + } + }); const msg = await session.sendAndWait( { prompt: "Search for lines starting with 'ap' in the file 'data.txt'. Tell me which lines matched.", @@ -138,6 +151,7 @@ describe("Built-in Tools", async () => { ); expect(msg?.data.content).toContain("apple"); expect(msg?.data.content).toContain("apricot"); + expect(grepCompletedSuccessfully).toBe(true); }, TEST_TIMEOUT_MS ); diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts index 35e7440766..bc3421bfa1 100644 --- a/nodejs/test/e2e/client.e2e.test.ts +++ b/nodejs/test/e2e/client.e2e.test.ts @@ -184,21 +184,15 @@ describe("Client", () => { await client.stop(); }); - it("should report error with stderr when CLI fails to start", async () => { + it.skipIf(isInProcessTransport)("should report error when CLI fails to start", async () => { const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ args: ["--nonexistent-flag-for-testing"] }), + connection: RuntimeConnection.forStdio({ + args: ["--nonexistent-flag-for-testing"], + }), }); onTestFinishedStop(client); - let initialError: Error | undefined; - try { - await client.start(); - expect.fail("Expected start() to throw an error"); - } catch (error) { - initialError = error as Error; - expect(initialError.message).toContain("stderr"); - expect(initialError.message).toContain("nonexistent"); - } + await expect(client.start()).rejects.toBeInstanceOf(Error); // Verify subsequent calls also fail (don't hang) try { @@ -206,7 +200,7 @@ describe("Client", () => { await session.send("test"); expect.fail("Expected send() to throw an error after CLI exit"); } catch (error) { - expect((error as Error).message).toContain("Connection is closed"); + expect(error).toBeInstanceOf(Error); } }); }); diff --git a/nodejs/test/e2e/extension_env_access.e2e.test.ts b/nodejs/test/e2e/extension_env_access.e2e.test.ts index f034db9051..38210a22fb 100644 --- a/nodejs/test/e2e/extension_env_access.e2e.test.ts +++ b/nodejs/test/e2e/extension_env_access.e2e.test.ts @@ -14,9 +14,9 @@ import { StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node.js"; -import { approveAll } from "../../src/index.js"; +import { approveAll, RuntimeConnection } from "../../src/index.js"; import { getSdkProtocolVersion } from "../../src/sdkProtocolVersion.js"; -import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; +import { createSdkTestContext, getLegacyCliPathForTests } from "./harness/sdkTestContext.js"; import { retry } from "./harness/sdkTestHelper.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -184,56 +184,47 @@ it("ignores a granted variable the extension never requested", async () => { expect(run.postjoin).toBe("E2E_SDK_TOKEN=granted-token\nE2E_SDK_SMUGGLED="); }); -const cliObservations = isInProcessTransport - ? "" - : mkdtempSync(join(tmpdir(), "copilot-env-access-cli-")); +const cliObservations = mkdtempSync(join(tmpdir(), "copilot-env-access-cli-")); const cliResultFile = join(cliObservations, "result"); -const cliContext = isInProcessTransport - ? undefined - : await createSdkTestContext({ - copilotClientOptions: { - env: { - COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS", - EXTENSION_ENV_REQUEST: "E2E_SDK_TOKEN", - EXTENSION_RESULT_FILE: cliResultFile, - EXTENSION_PREJOIN_FILE: join(cliObservations, "prejoin"), - EXTENSION_POSTJOIN_FILE: join(cliObservations, "postjoin"), - }, - }, - }); +const cliContext = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + env: { + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS", + EXTENSION_ENV_REQUEST: "E2E_SDK_TOKEN", + EXTENSION_RESULT_FILE: cliResultFile, + EXTENSION_PREJOIN_FILE: join(cliObservations, "prejoin"), + EXTENSION_POSTJOIN_FILE: join(cliObservations, "postjoin"), + }, + }, +}); // The released CLI ignores `requestedEnvironmentVariables`, so this covers the // half a real CLI can prove today: asking for variables does not break the join. // It becomes the grant test once `@github/copilot` carries the host half. -it.skipIf(isInProcessTransport)( - "joins a real CLI that does not support environment requests", - async () => { - if (!cliContext) { - throw new Error("Extension E2E requires an out-of-process transport"); - } - const { workDir, copilotClient } = cliContext; - const extensionDir = join(workDir, ".github", "extensions", "env-access"); - await rm(join(workDir, ".github"), { recursive: true, force: true }); - await rm(cliResultFile, { force: true }); - await mkdir(extensionDir, { recursive: true }); - await copyFile(FIXTURE, join(extensionDir, "extension.mjs")); - execFileSync("git", ["init", "--quiet"], { cwd: workDir }); - - await using _session = await copilotClient.createSession({ - requestExtensions: true, - extensionSdkPath: DIST_DIR, - onPermissionRequest: approveAll, - }); +it("joins a real CLI that does not support environment requests", async () => { + const { workDir, copilotClient } = cliContext; + const extensionDir = join(workDir, ".github", "extensions", "env-access"); + await rm(join(workDir, ".github"), { recursive: true, force: true }); + await rm(cliResultFile, { force: true }); + await mkdir(extensionDir, { recursive: true }); + await copyFile(FIXTURE, join(extensionDir, "extension.mjs")); + execFileSync("git", ["init", "--quiet"], { cwd: workDir }); + + await using _session = await copilotClient.createSession({ + requestExtensions: true, + extensionSdkPath: DIST_DIR, + onPermissionRequest: approveAll, + }); - await retry( - "wait for the env-access extension to join the session", - async () => { - expect(existsSync(cliResultFile)).toBe(true); - }, - 300, - 100 - ); + await retry( + "wait for the env-access extension to join the session", + async () => { + expect(existsSync(cliResultFile)).toBe(true); + }, + 300, + 100 + ); - expect(readFileSync(cliResultFile, "utf-8")).toBe("joined"); - } -); + expect(readFileSync(cliResultFile, "utf-8")).toBe("joined"); +}); diff --git a/nodejs/test/e2e/factory.e2e.test.ts b/nodejs/test/e2e/factory.e2e.test.ts index 669a1f9c64..2bf3ff17fb 100644 --- a/nodejs/test/e2e/factory.e2e.test.ts +++ b/nodejs/test/e2e/factory.e2e.test.ts @@ -4,30 +4,25 @@ import { copyFile, mkdir, rm } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { expect, it, vi } from "vitest"; -import { approveAll, FactoryResumeError } from "../../src/index.js"; +import { approveAll, FactoryResumeError, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN, - isInProcessTransport, + getLegacyCliPathForTests, } from "./harness/sdkTestContext.js"; import { retry } from "./harness/sdkTestHelper.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const factoryTestContext = isInProcessTransport - ? undefined - : await createSdkTestContext({ - copilotClientOptions: { - env: { - COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS,AGENT_FACTORIES", - }, - }, - }); +const factoryTestContext = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + env: { + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS,AGENT_FACTORIES", + }, + }, +}); async function setupFactoryExtension(workDir: string, onPermissionRequest = approveAll) { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { copilotClient, openAiEndpoint } = factoryTestContext; const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); const readyFile = join(extensionDir, "ready"); @@ -73,26 +68,20 @@ async function setupFactoryExtension(workDir: string, onPermissionRequest = appr return session; } -it.skipIf(isInProcessTransport)( - "runs an extension-authored factory across the SDK process boundary", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const result = await session.factory.run("argument-echo", { - args: { source: "sdk-e2e", count: 11 }, - notifyOnComplete: false, - }); +it("runs an extension-authored factory across the SDK process boundary", async () => { + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); - expect(result).toMatchObject({ - status: "completed", - result: { source: "sdk-e2e", count: 11 }, - }); - } -); + const result = await session.factory.run("argument-echo", { + args: { source: "sdk-e2e", count: 11 }, + notifyOnComplete: false, + }); + + expect(result).toMatchObject({ + status: "completed", + result: { source: "sdk-e2e", count: 11 }, + }); +}); // TODO(cli-1.0.81-2): the subagent request is rejected downstream under CLI 1.0.81-2, so the // fixture reports didThrow: true. Re-enable once the runtime fix ships. @@ -114,93 +103,78 @@ it.skip("forwards every declared subagent option to the runtime", async () => { }); }, 60_000); -it.skipIf(isInProcessTransport)( - "throws FactoryResumeError with not_found for an unknown run", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const error = await session.factory - .resume("00000000-0000-0000-0000-000000000000") - .catch((caught: unknown) => caught); - - expect(error).toBeInstanceOf(FactoryResumeError); - expect((error as FactoryResumeError).code).toBe("not_found"); - } -); - -it.skipIf(isInProcessTransport)( - "throws FactoryResumeError with non_resumable for a completed run", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const run = await session.factory.run("argument-echo", { notifyOnComplete: false }); - const error = await session.factory.resume(run.runId).catch((caught: unknown) => caught); - - expect(error).toBeInstanceOf(FactoryResumeError); - expect((error as FactoryResumeError).code).toBe("non_resumable"); - } -); - -it.skipIf(isInProcessTransport)( - "forwards factory runtime controls across the SDK process boundary", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const suppressed = await session.factory.run("phased", { - notifyOnComplete: false, - logPhaseNames: false, - }); - - expect(suppressed).toMatchObject({ - status: "completed", - result: "finished", - }); - const progress = await session.factory.getRunProgress(suppressed.runId); - expect(progress.records).toEqual( - expect.arrayContaining([ - expect.objectContaining({ kind: "phase", text: "Collect" }), - expect.objectContaining({ kind: "log", text: "Collected" }), - expect.objectContaining({ kind: "phase", text: "Summarize" }), - expect.objectContaining({ kind: "log", text: "Summarized" }), - ]) - ); - - const events = await session.getEvents(); - expect( - events.some( - (event) => - event.type === "system.notification" && - event.data.kind.type === "factory_completed" && - event.data.kind.runId === suppressed.runId - ) - ).toBe(false); - expect( - events.filter( - (event) => event.type === "session.info" && event.data.infoType === "factory_phase" - ) - ).toEqual([]); +it("throws FactoryResumeError with not_found for an unknown run", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const error = await session.factory + .resume("00000000-0000-0000-0000-000000000000") + .catch((caught: unknown) => caught); -it.skipIf(isInProcessTransport)("pages factory runs and returns cursor metadata", async () => { + expect(error).toBeInstanceOf(FactoryResumeError); + expect((error as FactoryResumeError).code).toBe("not_found"); +}); + +it("throws FactoryResumeError with non_resumable for a completed run", async () => { if (!factoryTestContext) { throw new Error("Factory E2E requires the stdio transport"); } const { workDir } = factoryTestContext; await using session = await setupFactoryExtension(workDir); + const run = await session.factory.run("argument-echo", { notifyOnComplete: false }); + const error = await session.factory.resume(run.runId).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(FactoryResumeError); + expect((error as FactoryResumeError).code).toBe("non_resumable"); +}); + +it("forwards factory runtime controls across the SDK process boundary", async () => { + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const suppressed = await session.factory.run("phased", { + notifyOnComplete: false, + logPhaseNames: false, + }); + + expect(suppressed).toMatchObject({ + status: "completed", + result: "finished", + }); + const progress = await session.factory.getRunProgress(suppressed.runId); + expect(progress.records).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "phase", text: "Collect" }), + expect.objectContaining({ kind: "log", text: "Collected" }), + expect.objectContaining({ kind: "phase", text: "Summarize" }), + expect.objectContaining({ kind: "log", text: "Summarized" }), + ]) + ); + + const events = await session.getEvents(); + expect( + events.some( + (event) => + event.type === "system.notification" && + event.data.kind.type === "factory_completed" && + event.data.kind.runId === suppressed.runId + ) + ).toBe(false); + expect( + events.filter( + (event) => event.type === "session.info" && event.data.infoType === "factory_phase" + ) + ).toEqual([]); +}); + +it("pages factory runs and returns cursor metadata", async () => { + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + const first = await session.factory.run("argument-echo", { args: { ordinal: 1 }, notifyOnComplete: false, @@ -244,178 +218,156 @@ it.skipIf(isInProcessTransport)("pages factory runs and returns cursor metadata" }); }); -it.skipIf(isInProcessTransport)( - "runs a factory when its session denies every permission request", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); - await using session = await setupFactoryExtension(workDir, denyPermissions); - - await expect( - session.factory.run("argument-echo", { notifyOnComplete: false }) - ).resolves.toMatchObject({ - status: "completed", - }); - expect(denyPermissions).not.toHaveBeenCalled(); +it("runs a factory when its session denies every permission request", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "resumes a failed factory when its session denies every permission request", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); - await using session = await setupFactoryExtension(workDir, denyPermissions); - - const failedRun = await session.factory.run("fails-once", { notifyOnComplete: false }); - expect(failedRun).toMatchObject({ - status: "error", - }); - - await expect( - session.factory.resume(failedRun.runId, { - notifyOnComplete: false, - logPhaseNames: false, - }) - ).resolves.toMatchObject({ - status: "completed", - result: "resumed", - }); - expect(denyPermissions).not.toHaveBeenCalled(); + const { workDir } = factoryTestContext; + const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); + await using session = await setupFactoryExtension(workDir, denyPermissions); + + await expect( + session.factory.run("argument-echo", { notifyOnComplete: false }) + ).resolves.toMatchObject({ + status: "completed", + }); + expect(denyPermissions).not.toHaveBeenCalled(); +}); + +it("resumes a failed factory when its session denies every permission request", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "refuses a factory started through the context session from a factory body", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const result = await session.factory.run("starts-from-context-session", { + const { workDir } = factoryTestContext; + const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); + await using session = await setupFactoryExtension(workDir, denyPermissions); + + const failedRun = await session.factory.run("fails-once", { notifyOnComplete: false }); + expect(failedRun).toMatchObject({ + status: "error", + }); + + await expect( + session.factory.resume(failedRun.runId, { notifyOnComplete: false, - }); + logPhaseNames: false, + }) + ).resolves.toMatchObject({ + status: "completed", + result: "resumed", + }); + expect(denyPermissions).not.toHaveBeenCalled(); +}); - expect(result).toMatchObject({ - status: "completed", - result: expect.stringContaining("factory.run and factory.resume"), - }); - expect((result as { result: string }).result).toContain("factory body"); +it("refuses a factory started through the context session from a factory body", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "refuses a factory started through the module session from a factory body", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const result = await session.factory.run("starts-from-module-session", { - notifyOnComplete: false, - }); + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const result = await session.factory.run("starts-from-context-session", { + notifyOnComplete: false, + }); + + expect(result).toMatchObject({ + status: "completed", + result: expect.stringContaining("factory.run and factory.resume"), + }); + expect((result as { result: string }).result).toContain("factory body"); +}); - expect(result).toMatchObject({ - status: "completed", - result: expect.stringContaining("factory.run and factory.resume"), - }); - expect((result as { result: string }).result).toContain("factory body"); +it("refuses a factory started through the module session from a factory body", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "allows a module-level extension watcher to start a factory while another body is parked", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); - await using session = await setupFactoryExtension(workDir); - - const parked = session.factory.run("parked", { notifyOnComplete: false }); - await retry( - "wait for the parked factory to enter its body", - async () => { - expect(existsSync(join(extensionDir, "entered"))).toBe(true); - }, - 100, - 100 - ); - - writeFileSync(join(extensionDir, "start-b"), "start"); - const bResultFile = join(extensionDir, "b-result"); - await retry( - "wait for the module-level watcher factory run to succeed", - async () => { - expect(existsSync(bResultFile)).toBe(true); - expect(JSON.parse(readFileSync(bResultFile, "utf8"))).toMatchObject({ - status: "success", - result: { - status: "completed", - result: { source: "module-watcher" }, - }, - }); - }, - 100, - 100 - ); - - writeFileSync(join(extensionDir, "release"), "release"); - await expect(parked).resolves.toMatchObject({ - status: "completed", - result: "released", - }); - }, - 60_000 -); - -it.skipIf(isInProcessTransport)( - "returns an array result from an extension-authored factory", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const result = await session.factory.run("array-result", { notifyOnComplete: false }); - - expect(result).toMatchObject({ - status: "completed", - result: [1, "two", false], - }); + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const result = await session.factory.run("starts-from-module-session", { + notifyOnComplete: false, + }); + + expect(result).toMatchObject({ + status: "completed", + result: expect.stringContaining("factory.run and factory.resume"), + }); + expect((result as { result: string }).result).toContain("factory body"); +}); + +it("allows a module-level extension watcher to start a factory while another body is parked", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "passes array factory arguments across the SDK process boundary", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const args = [1, "two", false]; - const result = await session.factory.run("argument-echo", { - args, - notifyOnComplete: false, - }); + const { workDir } = factoryTestContext; + const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); + await using session = await setupFactoryExtension(workDir); + + const parked = session.factory.run("parked", { notifyOnComplete: false }); + await retry( + "wait for the parked factory to enter its body", + async () => { + expect(existsSync(join(extensionDir, "entered"))).toBe(true); + }, + 100, + 100 + ); - expect(result).toMatchObject({ - status: "completed", - result: args, - }); + writeFileSync(join(extensionDir, "start-b"), "start"); + const bResultFile = join(extensionDir, "b-result"); + await retry( + "wait for the module-level watcher factory run to succeed", + async () => { + expect(existsSync(bResultFile)).toBe(true); + expect(JSON.parse(readFileSync(bResultFile, "utf8"))).toMatchObject({ + status: "success", + result: { + status: "completed", + result: { source: "module-watcher" }, + }, + }); + }, + 100, + 100 + ); + + writeFileSync(join(extensionDir, "release"), "release"); + await expect(parked).resolves.toMatchObject({ + status: "completed", + result: "released", + }); +}, 60_000); + +it("returns an array result from an extension-authored factory", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); + } + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const result = await session.factory.run("array-result", { notifyOnComplete: false }); + + expect(result).toMatchObject({ + status: "completed", + result: [1, "two", false], + }); +}); + +it("passes array factory arguments across the SDK process boundary", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const args = [1, "two", false]; + const result = await session.factory.run("argument-echo", { + args, + notifyOnComplete: false, + }); + + expect(result).toMatchObject({ + status: "completed", + result: args, + }); +}); diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index bf62db4826..58c275c800 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -50,6 +50,29 @@ function getCliPathForTests(): string | undefined { return undefined; } +function getCliPlatformPackageNames(): string[] { + const variants = + process.platform === "linux" + ? process.report?.getReport().header.glibcVersionRuntime + ? ["linux", "linuxmusl"] + : ["linuxmusl", "linux"] + : [process.platform]; + return variants.map((variant) => `@github/copilot-${variant}-${process.arch}`); +} + +/** Resolves the legacy SEA only for tests that explicitly exercise Node-hosted features. */ +export function getLegacyCliPathForTests(): string { + const cliName = process.platform === "win32" ? "copilot.exe" : "copilot"; + const githubModules = resolve(__dirname, "../../../node_modules/@github"); + for (const packageName of getCliPlatformPackageNames()) { + const cliPath = join(githubModules, packageName.slice("@github/".length), cliName); + if (fs.existsSync(cliPath)) { + return cliPath; + } + } + throw new Error("Legacy Copilot CLI binary not found in the installed platform package."); +} + export async function createSdkTestContext({ logLevel, useStdio, @@ -286,8 +309,13 @@ export async function createSdkTestContext({ process.chdir(restoreCwd); restoreCwd = undefined; } - // Empty directories but leave them in place for next test - await rimraf([join(homeDir, "*"), join(workDir, "*")], { glob: true }); + // The in-process runtime retains open state files until afterAll shuts it down. + // Keep its isolated home intact while it is alive; removing open files on POSIX + // can leave later tests using unlinked database state. + const cleanupPaths = isInProcess + ? [join(workDir, "*")] + : [join(homeDir, "*"), join(workDir, "*")]; + await rimraf(cleanupPaths, { glob: true }); }); afterAll(async () => { diff --git a/nodejs/test/e2e/inprocess_ffi.e2e.test.ts b/nodejs/test/e2e/inprocess_ffi.e2e.test.ts index af879ea77b..e3b5f75ee4 100644 --- a/nodejs/test/e2e/inprocess_ffi.e2e.test.ts +++ b/nodejs/test/e2e/inprocess_ffi.e2e.test.ts @@ -11,9 +11,8 @@ describe("In-process FFI transport", () => { // exercised by the full E2E suite running under the `inprocess` CI matrix cell, // not a dedicated test. it("should start and connect over in-process FFI", async () => { - // In-process FFI hosting resolves the CLI entrypoint (COPILOT_CLI_PATH or the - // bundled platform package) and its sibling native runtime library itself. If - // neither is available, start() throws and the test fails hard. + // In-process FFI hosting loads runtime.node directly from the bundled runtime. + // If it is unavailable, start() throws and the test fails hard. const client = new CopilotClient({ connection: RuntimeConnection.forInProcess() }); await client.start(); diff --git a/nodejs/test/e2e/pending_work_resume.e2e.test.ts b/nodejs/test/e2e/pending_work_resume.e2e.test.ts index 85abc3a900..7c2906c7b9 100644 --- a/nodejs/test/e2e/pending_work_resume.e2e.test.ts +++ b/nodejs/test/e2e/pending_work_resume.e2e.test.ts @@ -13,6 +13,7 @@ import type { PermissionRequestResult, } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; const PENDING_WORK_TIMEOUT_MS = 60_000; const TEST_TIMEOUT_MS = 180_000; @@ -516,7 +517,46 @@ describe("Pending work resume", async () => { ).toBe("beta"); if (scenario.disconnectOriginalClient) { - await suspendedClient.forceStop(); + const lockObserver = new CopilotClient({ + workingDirectory: workDir, + env, + gitHubToken: DEFAULT_GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ + path: process.env.COPILOT_CLI_PATH, + }), + }); + try { + await lockObserver.start(); + await waitForCondition( + async () => { + const result = await lockObserver.rpc.sessions.checkInUse({ + sessionIds: [sessionId], + }); + return result.inUse.includes(sessionId); + }, + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: `Timed out waiting for session '${sessionId}' to acquire its lock.`, + } + ); + + await suspendedClient.forceStop(); + + await waitForCondition( + async () => { + const result = await lockObserver.rpc.sessions.checkInUse({ + sessionIds: [sessionId], + }); + return !result.inUse.includes(sessionId); + }, + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: `Timed out waiting for session '${sessionId}' to release its lock.`, + } + ); + } finally { + await lockObserver.forceStop(); + } } const resumedClient = createConnectingClient(cliUrl); diff --git a/nodejs/test/e2e/rewind.e2e.test.ts b/nodejs/test/e2e/rewind.e2e.test.ts index 49c2b3b8f0..c64fe83145 100644 --- a/nodejs/test/e2e/rewind.e2e.test.ts +++ b/nodejs/test/e2e/rewind.e2e.test.ts @@ -2,13 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; const FILE_NAME = "rewind-sdk.txt"; +const ORIGINAL_FILE_CONTENT = "Original rewind content"; +const PREPARED_FILE_CONTENT = "Prepared rewind content"; const FILE_CONTENT = "SDK rewind content"; function expectSamePath(actual: string, expected: string): void { @@ -24,66 +26,73 @@ function expectSamePath(actual: string, expected: string): void { describe("Rewind", async () => { const { copilotClient: client, workDir } = await createSdkTestContext(); - // TODO(cli-1.0.81): Re-enable when Windows file-change tracking records built-in create tool writes. - it.skipIf(process.platform === "win32")( - "should restore tracked file and conversation", - async () => { - const filePath = join(workDir, FILE_NAME); - const session = await client.createSession({ - model: "claude-sonnet-4.5", - enableFileChangeTracking: true, - onPermissionRequest: approveAll, + it("should restore tracked file and conversation", async () => { + const filePath = join(workDir, FILE_NAME); + writeFileSync(filePath, ORIGINAL_FILE_CONTENT); + const session = await client.createSession({ + model: "claude-sonnet-4.5", + enableFileChangeTracking: true, + onPermissionRequest: approveAll, + }); + + try { + const ready = await session.sendAndWait({ + prompt: `Use the edit tool to replace the exact contents of ${FILE_NAME} from ${ORIGINAL_FILE_CONTENT} to ${PREPARED_FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_READY.`, }); + expect(ready?.data.content).toBe("SDK_REWIND_READY"); + expect(readFileSync(filePath, "utf8")).toBe(PREPARED_FILE_CONTENT); - try { - const response = await session.sendAndWait({ - prompt: `Use the create tool to create ${FILE_NAME} containing exactly ${FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_DONE.`, - }); + const response = await session.sendAndWait({ + prompt: `Use the edit tool to replace the exact contents of ${FILE_NAME} from ${PREPARED_FILE_CONTENT} to ${FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_DONE.`, + }); - expect(response?.data.content).toBe("SDK_REWIND_DONE"); - expect(existsSync(filePath)).toBe(true); - expect(readFileSync(filePath, "utf8")).toBe(FILE_CONTENT); + expect(response?.data.content).toBe("SDK_REWIND_DONE"); + expect(existsSync(filePath)).toBe(true); + expect(readFileSync(filePath, "utf8")).toBe(FILE_CONTENT); - let rewindPoints = await session.rpc.history.listRewindPoints(); - const deadline = Date.now() + 30_000; - while ( - Date.now() < deadline && - (rewindPoints.unavailableReason !== undefined || - !rewindPoints.points[0]?.canRestoreFiles) - ) { - await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); - rewindPoints = await session.rpc.history.listRewindPoints(); - } + let rewindPoints = await session.rpc.history.listRewindPoints(); + const deadline = Date.now() + 30_000; + while ( + Date.now() < deadline && + (rewindPoints.unavailableReason !== undefined || + rewindPoints.points.length !== 2 || + !rewindPoints.points[1]?.turnChangedFiles || + !rewindPoints.points[1]?.canRestoreFiles) + ) { + await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); + rewindPoints = await session.rpc.history.listRewindPoints(); + } - expect(rewindPoints.unavailableReason).toBeUndefined(); - expect(rewindPoints.fileChangeTrackingEnabled).toBe(true); - expect(rewindPoints.points).toHaveLength(1); - const rewindPoint = rewindPoints.points[0]; - expect(rewindPoint.canRestoreFiles).toBe(true); - expect(rewindPoint.fileCount).toBe(1); + expect(rewindPoints.unavailableReason).toBeUndefined(); + expect(rewindPoints.fileChangeTrackingEnabled).toBe(true); + expect(rewindPoints.points).toHaveLength(2); + const rewindPoint = rewindPoints.points[1]; + expect(rewindPoint.turnChangedFiles).toBe(true); + expect(rewindPoint.canRestoreFiles).toBe(true); + expect(rewindPoint.fileCount).toBe(1); - const preview = await session.rpc.history.previewRewind({ - eventId: rewindPoint.eventId, - }); - expect(preview.available).toBe(true); - expect(preview.files).toHaveLength(1); - expectSamePath(preview.files[0].path, filePath); + const preview = await session.rpc.history.previewRewind({ + eventId: rewindPoint.eventId, + }); + expect(preview.available).toBe(true); + expect(preview.files).toHaveLength(1); + expectSamePath(preview.files[0].path, filePath); - const rewind = await session.rpc.history.rewind({ - eventId: rewindPoint.eventId, - mode: "conversation-and-files", - }); - expect(rewind.outcome).toBe("success"); - expect(rewind.eventsRemoved).toBeGreaterThan(0); - expect(rewind.restoredFiles).toHaveLength(1); - expectSamePath(rewind.restoredFiles[0], filePath); - expect(existsSync(filePath)).toBe(false); + const rewind = await session.rpc.history.rewind({ + eventId: rewindPoint.eventId, + mode: "conversation-and-files", + }); + expect(rewind.outcome).toBe("success"); + expect(rewind.eventsRemoved).toBeGreaterThan(0); + expect(rewind.restoredFiles).toHaveLength(1); + expectSamePath(rewind.restoredFiles[0], filePath); + expect(existsSync(filePath)).toBe(true); + expect(readFileSync(filePath, "utf8")).toBe(PREPARED_FILE_CONTENT); - const events = await session.getEvents(); - expect(events.some((event) => event.id === rewindPoint.eventId)).toBe(false); - } finally { - await session.disconnect(); - } + const events = await session.getEvents(); + expect(events.some((event) => event.id === rewindPoint.eventId)).toBe(false); + } finally { + await session.disconnect(); } - ); + }); }); diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts index ad2adab109..6366db36cb 100644 --- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts +++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts @@ -5,7 +5,11 @@ import { afterAll, describe, expect, it } from "vitest"; import { CopilotClient, approveAll, RuntimeConnection } from "../../src/index.js"; import type { SessionEvent } from "../../src/index.js"; -import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; +import { + createSdkTestContext, + getLegacyCliPathForTests, + isInProcessTransport, +} from "./harness/sdkTestContext.js"; describe("UI Elicitation", async () => { const { copilotClient: client } = await createSdkTestContext(); @@ -43,22 +47,28 @@ describe("UI Elicitation Callback", async () => { "session created with the elicitation ask-user variant exposes the structured tool", { timeout: 60_000 }, async () => { - const session = await client.createSession({ - onPermissionRequest: approveAll, - askUserVariant: "elicitation", - onElicitationRequest: async () => ({ action: "accept", content: {} }), + const legacyClient = ctx.createClient({ + connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), }); + try { + const session = await legacyClient.createSession({ + onPermissionRequest: approveAll, + askUserVariant: "elicitation", + onElicitationRequest: async () => ({ action: "accept", content: {} }), + }); - await session.rpc.tools.initializeAndValidate(); - const { tools } = await session.rpc.tools.getCurrentMetadata(); - const askUserSchema = tools?.find((tool) => tool.name === "ask_user")?.input_schema as - | { properties?: Record } - | undefined; - - expect(askUserSchema?.properties).toHaveProperty("message"); - expect(askUserSchema?.properties).toHaveProperty("requestedSchema"); - expect(askUserSchema?.properties).not.toHaveProperty("question"); - await session.disconnect(); + await session.rpc.tools.initializeAndValidate(); + const { tools } = await session.rpc.tools.getCurrentMetadata(); + const askUserSchema = tools?.find((tool) => tool.name === "ask_user") + ?.input_schema as { properties?: Record } | undefined; + + expect(askUserSchema?.properties).toHaveProperty("message"); + expect(askUserSchema?.properties).toHaveProperty("requestedSchema"); + expect(askUserSchema?.properties).not.toHaveProperty("question"); + await session.disconnect(); + } finally { + await legacyClient.stop(); + } } ); diff --git a/nodejs/test/runtimeArtifacts.test.ts b/nodejs/test/runtimeArtifacts.test.ts new file mode 100644 index 0000000000..4a58789e6e --- /dev/null +++ b/nodejs/test/runtimeArtifacts.test.ts @@ -0,0 +1,109 @@ +import { existsSync, mkdtempSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { defaultRuntimeCacheRoot, materializeRuntimeBundle } from "../src/runtimeArtifacts.js"; + +describe("defaultRuntimeCacheRoot", () => { + it.each([ + [ + "darwin", + "/home/test", + {}, + join("/home/test", "Library", "Caches", "github-copilot-sdk", "runtime"), + ], + ["linux", "/home/test", {}, join("/home/test", ".cache", "github-copilot-sdk", "runtime")], + [ + "linux", + "/home/test", + { XDG_CACHE_HOME: "/cache" }, + join("/cache", "github-copilot-sdk", "runtime"), + ], + [ + "win32", + "C:\\Users\\test", + { LOCALAPPDATA: "C:\\Users\\test\\AppData\\Local" }, + join("C:\\Users\\test\\AppData\\Local", "github-copilot-sdk", "runtime"), + ], + ])("uses the %s user cache directory", (platform, home, environment, expected) => { + expect(defaultRuntimeCacheRoot(platform, home, environment)).toBe(expected); + }); +}); + +describe("materializeRuntimeBundle", () => { + afterEach(() => vi.unstubAllEnvs()); + + it("materializes an adjacent pair from an absent cache with a stripped environment", () => { + const sourceDir = mkdtempSync(join(tmpdir(), "copilot-runtime-source-")); + const cacheRoot = join(sourceDir, "absent-cache"); + const emptyPath = join(sourceDir, "empty-path"); + mkdirSync(emptyPath); + const wrapperName = + process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; + const prebuilds = join(sourceDir, "prebuilds", "test-platform"); + const wrapper = join(prebuilds, wrapperName); + const runtimeNode = join(prebuilds, "runtime.node"); + mkdirSync(prebuilds, { recursive: true }); + writeFileSync(wrapper, "wrapper"); + writeFileSync(runtimeNode, "runtime"); + mkdirSync(join(sourceDir, "ripgrep", "bin", "test-platform"), { recursive: true }); + writeFileSync(join(sourceDir, "ripgrep", "bin", "test-platform", "rg"), "ripgrep"); + mkdirSync(join(sourceDir, "definitions"), { recursive: true }); + writeFileSync(join(sourceDir, "definitions", "future.json"), "{}"); + writeFileSync(join(sourceDir, "app.js"), "excluded"); + writeFileSync(join(sourceDir, "copilot"), "excluded"); + writeFileSync(join(sourceDir, "copilot.exe"), "excluded"); + writeFileSync(join(sourceDir, "LICENSE.md"), "excluded"); + writeFileSync(join(sourceDir, "README.md"), "excluded"); + + vi.stubEnv("PATH", emptyPath); + vi.stubEnv("COPILOT_CLI_PATH", undefined); + vi.stubEnv("COPILOT_RUNTIME_HOST_COMMAND", undefined); + vi.stubEnv("COPILOT_RUNTIME_PROVIDER_LIB", undefined); + + expect(process.env.COPILOT_CLI_PATH).toBeUndefined(); + expect(process.env.COPILOT_RUNTIME_HOST_COMMAND).toBeUndefined(); + expect(process.env.COPILOT_RUNTIME_PROVIDER_LIB).toBeUndefined(); + + const installedWrapper = materializeRuntimeBundle( + { packageRoot: sourceDir, platform: "test-platform" }, + cacheRoot + ); + const installDir = dirname(installedWrapper); + + expect(readFileSync(installedWrapper, "utf8")).toBe("wrapper"); + expect(readFileSync(join(installDir, "runtime.node"), "utf8")).toBe("runtime"); + expect( + readFileSync(join(installDir, "ripgrep", "bin", "test-platform", "rg"), "utf8") + ).toBe("ripgrep"); + expect(existsSync(join(installDir, "app.js"))).toBe(false); + expect(existsSync(join(installDir, "copilot"))).toBe(false); + expect(existsSync(join(installDir, "copilot.exe"))).toBe(false); + expect(existsSync(join(installDir, "LICENSE.md"))).toBe(false); + expect(existsSync(join(installDir, "README.md"))).toBe(false); + if (process.platform !== "win32") { + expect(statSync(installedWrapper).mode & 0o111).not.toBe(0); + } + }); + + it("fails clearly when the package has no runtime.node", () => { + const sourceDir = mkdtempSync(join(tmpdir(), "copilot-runtime-missing-node-")); + const wrapperName = + process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; + const prebuilds = join(sourceDir, "prebuilds", "test-platform"); + const wrapper = join(prebuilds, wrapperName); + mkdirSync(prebuilds, { recursive: true }); + writeFileSync(wrapper, "wrapper"); + + expect(() => + materializeRuntimeBundle( + { + packageRoot: sourceDir, + platform: "test-platform", + }, + join(sourceDir, "cache") + ) + ).toThrow(/Copilot runtime\.node not found/); + }); +}); diff --git a/python/README.md b/python/README.md index c130f21c65..5cb1dc03c9 100644 --- a/python/README.md +++ b/python/README.md @@ -29,8 +29,9 @@ runtime: python -m copilot download-runtime ``` -This caches the runtime binary locally. If you skip this step, the SDK will -attempt to download it automatically on first use as a fallback. +This caches `copilot-runtime`, its adjacent `runtime.node`, and the compatible +`copilot` host locally. If you skip this step, the SDK downloads the bundle +automatically on first managed stdio/TCP use. To pre-provision the native library required by the in-process (FFI) transport (see [In-process (FFI) transport](#in-process-ffi-transport)), pass `--in-process`: @@ -39,15 +40,15 @@ To pre-provision the native library required by the in-process (FFI) transport python -m copilot download-runtime --in-process ``` -This additionally fetches the native runtime library into the versioned runtime -cache. Stdio/TCP users never download it. When omitted, it is downloaded -lazily on first use of the in-process transport. +This instead provisions the compatible CLI artifact and native runtime library +used by in-process hosting. When omitted, they are downloaded lazily on first +use of the in-process transport. | Platform | Cache path | |----------|-----------| -| Linux | `~/.cache/github-copilot-sdk/cli//copilot` | -| macOS | `~/Library/Caches/github-copilot-sdk/cli//copilot` | -| Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\copilot.exe` | +| Linux | `~/.cache/github-copilot-sdk/cli//prebuilds//` | +| macOS | `~/Library/Caches/github-copilot-sdk/cli//prebuilds//` | +| Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\prebuilds\\` | ### Environment variables @@ -56,7 +57,8 @@ lazily on first use of the in-process transport. | `COPILOT_CLI_PATH` | Use this specific binary instead of downloading | | `COPILOT_CLI_EXTRACT_DIR` | Override the cache directory (binary placed directly here) | | `COPILOT_SKIP_CLI_DOWNLOAD` | Set to `1` to disable auto-download | -| `COPILOT_CLI_DOWNLOAD_BASE_URL` | Override the GitHub Releases download URL | +| `COPILOT_NPM_REGISTRY_URL` | Override the npm registry used for managed out-of-process and in-process runtime downloads | +| `COPILOT_CLI_DOWNLOAD_BASE_URL` | Override the GitHub Releases download URL used for the root CLI | ## Run the Sample @@ -223,6 +225,10 @@ All options are kw-only parameters: - `RuntimeConnection.for_uri(url, connection_token=None)` — connect to an existing CLI server (e.g. `"localhost:8080"`). - `RuntimeConnection.for_inprocess()` — host the runtime in-process via its native C ABI (FFI). See [In-process (FFI) transport](#in-process-ffi-transport). +Managed stdio and TCP connections use the downloaded `copilot-runtime` +executable with adjacent `runtime.node` by default. An explicit connection +path or `COPILOT_CLI_PATH` overrides the downloaded runtime. + Child-process connections (`for_stdio`/`for_tcp`) also expose a per-connection `env` field for the spawned process. Set it on the returned connection instead of the client-level `env` — setting both raises: diff --git a/python/copilot/_cli_download.py b/python/copilot/_cli_download.py index b831e072ad..4477fcfff3 100644 --- a/python/copilot/_cli_download.py +++ b/python/copilot/_cli_download.py @@ -27,7 +27,7 @@ import tempfile import time import zipfile -from pathlib import Path +from pathlib import Path, PurePosixPath from urllib.error import HTTPError, URLError from urllib.request import urlopen @@ -373,6 +373,164 @@ def _extract_runtime_node(data: bytes, npm_platform: str) -> bytes: raise RuntimeError(f"'{target}' not found in runtime package for {npm_platform}.") +def _extract_runtime_wrapper(data: bytes, npm_platform: str) -> bytes: + """Extract the SDK out-of-process wrapper from an npm platform tarball.""" + wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime" + target = f"package/prebuilds/{npm_platform}/{wrapper_name}" + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf: + for name in tf.getnames(): + if name == target or name.endswith(f"/prebuilds/{npm_platform}/{wrapper_name}"): + member = tf.getmember(name) + extracted = tf.extractfile(member) + if extracted is not None: + return extracted.read() + raise RuntimeError(f"'{target}' not found in runtime package for {npm_platform}.") + + +_HOSTLESS_EXCLUDED_TOP_LEVEL = { + "app.js", + "assets", + "changelog.json", + "copilot", + "copilot.exe", + "copilot-sdk", + "foundry-local-sdk", + "index.js", + "LICENSE.md", + "napi-oop-runtime", + "npm-loader.js", + "package.json", + "preloads", + "pvrecorder", + "queries", + "README.md", + "sdk", + "sea-loader.js", + "webview", +} + + +def _hostless_runtime_path(member_name: str, npm_platform: str) -> Path | None: + parts = PurePosixPath(member_name).parts + if not parts or parts[0] != "package" or len(parts) < 2: + return None + relative = parts[1:] + top_level = relative[0] + file_name = relative[-1] + if ( + top_level in _HOSTLESS_EXCLUDED_TOP_LEVEL + or (top_level.startswith("tree-sitter") and top_level.endswith(".wasm")) + or (top_level.startswith("voice-") and top_level.endswith(".js")) + or file_name == "cli-native.node" + or "mediaremote-adapter" in relative + or file_name.startswith("copilot-runtime-bin") + ): + return None + if top_level == "prebuilds": + if len(relative) < 3 or relative[1] != npm_platform: + return None + relative = relative[2:] + destination = Path(*relative) + if destination.is_absolute() or ".." in destination.parts: + raise RuntimeError(f"Unsafe runtime package path: {member_name}") + return destination + + +def _extract_runtime_bundle(data: bytes, npm_platform: str, destination: Path) -> None: + """Extract the hostless runtime tree, retaining unknown package assets by default.""" + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive: + for member in archive: + relative = _hostless_runtime_path(member.name, npm_platform) + if relative is None or member.isdir(): + continue + if not member.isfile(): + raise RuntimeError(f"Unsupported runtime package entry: {member.name}") + extracted = archive.extractfile(member) + if extracted is None: + raise RuntimeError(f"Failed to read runtime package entry: {member.name}") + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(extracted.read()) + if sys.platform != "win32": + target.chmod(member.mode & 0o777) + + +def ensure_runtime_wrapper(version: str | None = None, force: bool = False) -> str: + """Provision the runtime pair and its retained npm package assets.""" + ver = version or CLI_VERSION + if not ver: + raise RuntimeError("No runtime version is pinned.") + npm_platform = get_npm_platform() + wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime" + pair_dir = get_cache_dir(ver) / "prebuilds" / npm_platform + wrapper_path = pair_dir / wrapper_name + runtime_path = pair_dir / "runtime.node" + assets_marker = pair_dir / ".hostless-runtime-assets-v2" + + wrapper_exists = wrapper_path.is_file() and wrapper_path.stat().st_size > 0 + runtime_exists = runtime_path.is_file() and runtime_path.stat().st_size > 0 + if wrapper_exists and runtime_exists and assets_marker.is_file() and not force: + return str(wrapper_path) + if not force and wrapper_exists != runtime_exists: + raise RuntimeError( + f"Incomplete Copilot runtime bundle in {pair_dir}: " + f"{wrapper_name} and runtime.node are required." + ) + if _should_skip_download(): + raise RuntimeError( + f"Copilot runtime bundle is not cached in {pair_dir} " + "and automatic downloads are disabled." + ) + + data = _fetch_url_bytes(get_runtime_lib_url(ver, npm_platform), timeout=600) + integrity = _fetch_runtime_integrity(npm_platform, ver) + if not integrity: + raise RuntimeError( + "No Subresource Integrity value available for the Copilot runtime " + f"package ({npm_platform}@{ver}); refusing to stage unverified native code." + ) + _verify_integrity(data, integrity) + import shutil + + pair_dir.parent.mkdir(parents=True, exist_ok=True) + staging_dir = Path(tempfile.mkdtemp(dir=pair_dir.parent, prefix=".runtime-bundle-")) + try: + _extract_runtime_bundle(data, npm_platform, staging_dir) + staged_wrapper = staging_dir / wrapper_name + staged_runtime = staging_dir / "runtime.node" + if ( + not staged_wrapper.is_file() + or staged_wrapper.stat().st_size == 0 + or not staged_runtime.is_file() + or staged_runtime.stat().st_size == 0 + ): + raise RuntimeError("Copilot runtime wrapper and runtime.node must both be non-empty.") + if sys.platform != "win32": + staged_wrapper.chmod( + staged_wrapper.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + ) + (staging_dir / assets_marker.name).write_text("1\n", encoding="ascii") + try: + if pair_dir.exists() and (force or not assets_marker.is_file()): + shutil.rmtree(pair_dir, ignore_errors=True) + staging_dir.replace(pair_dir) + except OSError: + if ( + wrapper_path.is_file() + and wrapper_path.stat().st_size > 0 + and runtime_path.is_file() + and runtime_path.stat().st_size > 0 + and assets_marker.is_file() + ): + return str(wrapper_path) + raise + finally: + if staging_dir.exists(): + shutil.rmtree(staging_dir, ignore_errors=True) + + return str(wrapper_path) + + def ensure_runtime_library(cli_path: str, version: str | None = None) -> str | None: """Ensure the native in-process (FFI) runtime library sits next to ``cli_path``. @@ -536,7 +694,10 @@ def main() -> None: print(f"Downloading Copilot runtime v{ver}...") try: - path = download_cli(ver, force=args.force) + if args.in_process: + path = download_cli(ver, force=args.force) + else: + path = ensure_runtime_wrapper(ver, force=args.force) print(f"Runtime cached at: {path}") if args.in_process: print("Downloading in-process (FFI) runtime library...") diff --git a/python/copilot/_ffi_runtime_host.py b/python/copilot/_ffi_runtime_host.py index e04d1655e6..98aa776600 100644 --- a/python/copilot/_ffi_runtime_host.py +++ b/python/copilot/_ffi_runtime_host.py @@ -3,9 +3,9 @@ Instead of spawning the Copilot CLI as a child process and talking JSON-RPC over stdio/TCP, the in-process transport loads the runtime's native shared library (``runtime.node`` — a Rust ``cdylib``) into this process and drives JSON-RPC over -its C ABI (FFI). The native ``host_start`` export spawns the residual worker -itself, so the SDK never launches the worker directly; it only pumps opaque LSP -``Content-Length:``-framed JSON-RPC bytes across the boundary: +its C ABI (FFI). The native ``host_start`` export constructs the Rust server +synchronously; the SDK only pumps opaque LSP ``Content-Length:``-framed JSON-RPC +bytes across the boundary: - client → server frames go to ``copilot_runtime_connection_write`` - server → client frames arrive on a native callback that feeds a thread-safe @@ -114,8 +114,8 @@ def _natural_library_name() -> str: return "libcopilot_runtime.so" -def resolve_library_path(cli_entrypoint: str) -> str | None: - """Resolve the native runtime library next to the given CLI entrypoint. +def resolve_library_path(runtime_entrypoint: str) -> str | None: + """Resolve the native runtime library next to the given runtime entrypoint. Checks, in order: @@ -125,7 +125,7 @@ def resolve_library_path(cli_entrypoint: str) -> str | None: Returns the absolute path, or ``None`` when neither exists. """ - directory = Path(cli_entrypoint).resolve().parent + directory = Path(runtime_entrypoint).resolve().parent flat = directory / _natural_library_name() if flat.is_file(): @@ -327,15 +327,15 @@ def wait(self, timeout: float | None = None) -> int: # noqa: ARG002 class FfiRuntimeHost: """Hosts the Copilot runtime in-process via its native C ABI. - Construct with :meth:`create`, then :meth:`start` to spawn the worker and open - the FFI connection. Expose :attr:`process` to :class:`JsonRpcClient`, and call - :meth:`dispose` to tear everything down. + Construct with :meth:`create`, then :meth:`start` to start the native engine + and open the FFI connection. Expose :attr:`process` to + :class:`JsonRpcClient`, and call :meth:`dispose` to tear everything down. """ def __init__( self, library_path: str, - cli_entrypoint: str, + cli_entrypoint: str | None, environment: dict[str, str] | None = None, args: Sequence[str] = (), ) -> None: @@ -367,31 +367,30 @@ def process(self) -> _FfiProcessAdapter: @staticmethod def create( - cli_entrypoint: str, + library_path: str, + cli_entrypoint: str | None = None, environment: dict[str, str] | None = None, args: Sequence[str] = (), ) -> FfiRuntimeHost: - """Resolve the cdylib next to the CLI entrypoint and prepare the host. + """Load the runtime cdylib and prepare the host. Raises: RuntimeError: If the native runtime library cannot be found. """ - full_entrypoint = str(Path(cli_entrypoint).resolve()) - library_path = resolve_library_path(full_entrypoint) - if library_path is None: + full_library_path = str(Path(library_path).resolve()) + if not Path(full_library_path).is_file(): raise RuntimeError( - "In-process FFI runtime library not found next to " - f"'{full_entrypoint}'. Download it with " - "`python -m copilot download-runtime --in-process`, or set " - "COPILOT_CLI_PATH to a runtime package that ships it." + f"In-process FFI runtime library not found at '{full_library_path}'." ) - return FfiRuntimeHost(library_path, full_entrypoint, environment, args) + full_entrypoint = ( + str(Path(cli_entrypoint).resolve()) if cli_entrypoint is not None else None + ) + return FfiRuntimeHost(full_library_path, full_entrypoint, environment, args) def _build_argv(self) -> bytes: - # A `.js` entrypoint (dev) is launched via node; the packaged single-file - # CLI embeds its own Node and is invoked directly. `--no-auto-update` - # pins the worker to the runtime package matching the loaded cdylib. - if self._cli_entrypoint.lower().endswith(".js"): + if self._cli_entrypoint is None: + argv: list[str] = [] + elif self._cli_entrypoint.lower().endswith(".js"): argv = ["node", self._cli_entrypoint, "--embedded-host", "--no-auto-update"] else: argv = [self._cli_entrypoint, "--embedded-host", "--no-auto-update"] @@ -407,11 +406,9 @@ def _build_env(self) -> bytes | None: return json.dumps(obj).encode("utf-8") def start_blocking(self) -> None: - """Spawn the worker and open the FFI connection (blocks up to ~30s). + """Start the native engine and open the FFI connection. - Must be run off the event loop (e.g. via :func:`asyncio.to_thread`); - ``host_start`` blocks until the worker connects back and signals - readiness. + Must be run off the event loop (e.g. via :func:`asyncio.to_thread`). """ argv = self._build_argv() env = self._build_env() @@ -419,8 +416,7 @@ def start_blocking(self) -> None: self._server_id = self._lib.host_start(argv, len(argv), env, len(env) if env else 0) if not self._server_id: raise RuntimeError( - f"copilot_runtime_host_start failed (library '{self._library_path}', " - f"entrypoint '{self._cli_entrypoint}')." + f"copilot_runtime_host_start failed (library '{self._library_path}')." ) self._outbound_callback = _OutboundCallback(self._on_outbound) diff --git a/python/copilot/client.py b/python/copilot/client.py index 05412246de..1c0691b612 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -28,6 +28,7 @@ from collections.abc import Awaitable, Callable, Mapping, Sequence from dataclasses import dataclass, field from datetime import UTC, datetime +from pathlib import Path from types import TracebackType from typing import Any, ClassVar, Literal, NotRequired, TypedDict, cast, overload @@ -1356,25 +1357,6 @@ def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent: _CLI_PROCESS_EXIT_TIMEOUT_SECONDS = 5 -def _get_or_download_cli(*, include_runtime_lib: bool = False) -> str | None: - """Get the cached CLI binary, downloading if necessary. - - Returns the path to the CLI binary, or None if unavailable (dev install - with no pinned version, or auto-download disabled). - - When ``include_runtime_lib`` is set, also ensures the native in-process FFI - runtime is available (downloading it on first use). - """ - from ._cli_download import get_or_download_cli - - cli_path = get_or_download_cli() - if cli_path and include_runtime_lib: - from ._cli_download import ensure_runtime_library - - ensure_runtime_library(cli_path) - return cli_path - - def _extract_transform_callbacks( system_message: SystemMessageConfig | dict[str, Any] | None, ) -> tuple[dict[str, Any] | None, dict[str, SectionTransformFn] | None]: @@ -1651,6 +1633,7 @@ def __init__( self._cli_path_source: str | None = None self._ffi_host: FfiRuntimeHost | None = None self._inprocess_runtime_path: str | None = None + self._inprocess_cli_entrypoint: str | None = None if isinstance(connection, UriRuntimeConnection): if connection.connection_token is not None and len(connection.connection_token) == 0: @@ -1662,9 +1645,7 @@ def __init__( # In-process (FFI): no child process and no per-connection token. self._runtime_port = None self._effective_connection_token = None - self._inprocess_runtime_path = self._resolve_runtime_entrypoint( - None, include_runtime_lib=True - ) + self._inprocess_runtime_path = self._resolve_inprocess_runtime() if options.use_logged_in_user is None: options.use_logged_in_user = not bool(options.github_token) else: @@ -1685,7 +1666,7 @@ def __init__( else: self._effective_connection_token = None - # Resolve CLI path: explicit > COPILOT_CLI_PATH env var > downloaded binary. + # Resolve runtime path: explicit CLI > COPILOT_CLI_PATH > downloaded runtime. # Select the environment by identity, not truthiness, so an intentionally # empty per-connection or client env stays authoritative (the spawned child # receives that empty mapping) instead of falling back to os.environ and @@ -1730,52 +1711,47 @@ def _resolve_runtime_entrypoint( path: str | None, *, env: Mapping[str, str] | None = None, - include_runtime_lib: bool = False, ) -> str: """Resolve the runtime executable path (explicit > env > downloaded). Sets ``self._cli_path_source`` for diagnostics. When - ``include_runtime_lib`` is set (in-process transport), also ensures the - native runtime library is downloaded alongside the CLI. - Raises: RuntimeError: If no runtime path can be resolved. """ if path is not None: self._cli_path_source = "explicit" - return self._ensure_runtime_lib(path) if include_runtime_lib else path + return path lookup = env if env is not None else os.environ env_cli_path = lookup.get("COPILOT_CLI_PATH") if env_cli_path: self._cli_path_source = "environment" - return self._ensure_runtime_lib(env_cli_path) if include_runtime_lib else env_cli_path - - downloaded_path = _get_or_download_cli(include_runtime_lib=include_runtime_lib) - if downloaded_path: - self._cli_path_source = "downloaded" - return downloaded_path - - raise RuntimeError( - "Copilot CLI not found. Install a published wheel (which " - "auto-downloads the CLI on first use), set COPILOT_CLI_PATH, " - "or pass an explicit path via " - "RuntimeConnection.for_stdio(path=...) / " - "RuntimeConnection.for_tcp(path=...)." - ) + return env_cli_path - @staticmethod - def _ensure_runtime_lib(cli_path: str) -> str: - """Ensure the in-process runtime library sits next to a user-supplied CLI. + from ._cli_download import ensure_runtime_wrapper - For explicit/``COPILOT_CLI_PATH`` entrypoints, the native library may - already be bundled (dev ``prebuilds`` layout); otherwise it is fetched on - first use. Returns ``cli_path`` unchanged. - """ - from ._cli_download import ensure_runtime_library + self._cli_path_source = "downloaded" + return ensure_runtime_wrapper() - ensure_runtime_library(cli_path) - return cli_path + def _resolve_inprocess_runtime(self) -> str: + explicit_cli = os.environ.get("COPILOT_CLI_PATH") + if explicit_cli: + from ._cli_download import ensure_runtime_library + + runtime_path = ensure_runtime_library(explicit_cli) + if runtime_path is None: + raise RuntimeError( + f"In-process runtime library not found next to '{explicit_cli}'." + ) + self._cli_path_source = "environment" + self._inprocess_cli_entrypoint = explicit_cli + return runtime_path + + from ._cli_download import ensure_runtime_wrapper + + wrapper_path = Path(ensure_runtime_wrapper()) + self._cli_path_source = "downloaded" + return str(wrapper_path.with_name("runtime.node")) @property def rpc(self) -> ServerRpc: @@ -4306,7 +4282,6 @@ async def _start_cli_server(self) -> None: env = dict(os.environ) else: env = dict(opts.env) - # Set auth token in environment if provided if opts.github_token: env["COPILOT_SDK_AUTH_TOKEN"] = opts.github_token @@ -4457,6 +4432,7 @@ async def _start_inprocess_ffi(self) -> None: host = FfiRuntimeHost.create( runtime_path, + cli_entrypoint=self._inprocess_cli_entrypoint, environment=environment or None, args=tuple(args), ) diff --git a/python/e2e/conftest.py b/python/e2e/conftest.py index f441097f32..a789b2b567 100644 --- a/python/e2e/conftest.py +++ b/python/e2e/conftest.py @@ -1,10 +1,14 @@ """Shared pytest fixtures for e2e tests.""" +import json import os +from pathlib import Path import pytest import pytest_asyncio +import copilot._cli_download as cli_download + from .testharness import E2ETestContext, is_inprocess_transport # Host-side auth resolution ranks HMAC above the GitHub token, so an ambient @@ -15,9 +19,16 @@ # .NET's InProcessEnvIsolation [ModuleInitializer] and Node's module-init guard. # Out-of-process children resolve auth in their own process where the token already # outranks HMAC. See https://github.com/github/copilot-sdk/issues/1934. +if not cli_download.CLI_VERSION: + package_lock = json.loads( + (Path(__file__).parents[2] / "nodejs" / "package-lock.json").read_text() + ) + cli_download.CLI_VERSION = package_lock["packages"]["node_modules/@github/copilot"]["version"] + if is_inprocess_transport(): os.environ.pop("COPILOT_HMAC_KEY", None) os.environ.pop("CAPI_HMAC_KEY", None) + os.environ.pop("COPILOT_CLI_PATH", None) @pytest.hookimpl(tryfirst=True, hookwrapper=True) diff --git a/python/e2e/test_inprocess_ffi_e2e.py b/python/e2e/test_inprocess_ffi_e2e.py index c119c4ea4e..ea82037b7a 100644 --- a/python/e2e/test_inprocess_ffi_e2e.py +++ b/python/e2e/test_inprocess_ffi_e2e.py @@ -15,20 +15,14 @@ from copilot import CopilotClient, RuntimeConnection from .testharness import E2ETestContext -from .testharness.context import get_cli_path_for_tests pytestmark = pytest.mark.asyncio(loop_scope="module") class TestInProcessFfi: - async def test_should_start_and_connect_over_in_process_ffi( - self, ctx: E2ETestContext, monkeypatch: pytest.MonkeyPatch - ): - # In-process hosting loads the runtime cdylib next to the resolved CLI - # entrypoint and lets the native host spawn the worker. ``ping`` is a - # purely local RPC round-trip, so no auth or replay proxy is involved. - # If the native library is unavailable, start() raises and the test fails. - monkeypatch.setenv("COPILOT_CLI_PATH", get_cli_path_for_tests()) + async def test_should_start_and_connect_over_in_process_ffi(self, ctx: E2ETestContext): + # In-process hosting loads runtime.node directly. ``ping`` is a purely local + # RPC round-trip, so no auth or replay proxy is involved. client = CopilotClient(connection=RuntimeConnection.for_inprocess()) await client.start() diff --git a/python/e2e/test_mode_handlers_e2e.py b/python/e2e/test_mode_handlers_e2e.py index f6173a4a5e..d5182c453d 100644 --- a/python/e2e/test_mode_handlers_e2e.py +++ b/python/e2e/test_mode_handlers_e2e.py @@ -6,6 +6,7 @@ import pytest +from copilot.rpc import ModeSetRequest from copilot.session import PermissionHandler from copilot.session_events import ( AutoModeSwitchCompletedData, @@ -15,6 +16,7 @@ ExitPlanModeCompletedData, ExitPlanModeRequestedData, SessionIdleData, + SessionMode, SessionModelChangeData, ) @@ -111,10 +113,8 @@ async def on_exit_plan_mode_request(request, invocation): ) ) - response = await session.send_and_wait( - PLAN_PROMPT, - agent_mode="plan", - ) + await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.PLAN)) + response = await session.send_and_wait(PLAN_PROMPT) assert len(exit_plan_mode_requests) == 1 request = exit_plan_mode_requests[0] diff --git a/python/e2e/test_rewind_e2e.py b/python/e2e/test_rewind_e2e.py index 10e7bc4dd9..fa87c08b64 100644 --- a/python/e2e/test_rewind_e2e.py +++ b/python/e2e/test_rewind_e2e.py @@ -4,7 +4,6 @@ import asyncio import os -import sys from pathlib import Path import pytest @@ -22,6 +21,8 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") FILE_NAME = "rewind-sdk.txt" +ORIGINAL_FILE_CONTENT = "Original rewind content" +PREPARED_FILE_CONTENT = "Prepared rewind content" FILE_CONTENT = "SDK rewind content" @@ -31,10 +32,8 @@ def _same_path(left: str | Path, right: str | Path) -> bool: class TestRewind: async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestContext): - if sys.platform == "win32": - pytest.skip("blocked on CLI 1.0.81 file-change tracking regression on Windows") - file_path = Path(ctx.work_dir) / FILE_NAME + file_path.write_text(ORIGINAL_FILE_CONTENT, encoding="utf-8") session = await ctx.client.create_session( model="claude-sonnet-4.5", enable_file_change_tracking=True, @@ -42,8 +41,18 @@ async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestCo ) try: + ready = await session.send_and_wait( + f"Use the edit tool to replace the exact contents of {FILE_NAME} " + f"from {ORIGINAL_FILE_CONTENT} to {PREPARED_FILE_CONTENT}. " + "After the tool succeeds, reply with exactly SDK_REWIND_READY." + ) + assert ready is not None + assert ready.data.content == "SDK_REWIND_READY" + assert file_path.read_text(encoding="utf-8") == PREPARED_FILE_CONTENT + response = await session.send_and_wait( - f"Use the create tool to create {FILE_NAME} containing exactly {FILE_CONTENT}. " + f"Use the edit tool to replace the exact contents of {FILE_NAME} " + f"from {PREPARED_FILE_CONTENT} to {FILE_CONTENT}. " "After the tool succeeds, reply with exactly SDK_REWIND_DONE." ) @@ -59,16 +68,18 @@ async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestCo deadline = asyncio.get_running_loop().time() + 30 while asyncio.get_running_loop().time() < deadline and not ( rewind_points.unavailable_reason is None - and rewind_points.points - and rewind_points.points[0].can_restore_files + and len(rewind_points.points) == 2 + and rewind_points.points[1].turn_changed_files + and rewind_points.points[1].can_restore_files ): await asyncio.sleep(0.1) rewind_points = await session.rpc.history.list_rewind_points() assert rewind_points.unavailable_reason is None assert rewind_points.file_change_tracking_enabled - assert len(rewind_points.points) == 1 - rewind_point = rewind_points.points[0] + assert len(rewind_points.points) == 2 + rewind_point = rewind_points.points[1] + assert rewind_point.turn_changed_files assert rewind_point.can_restore_files assert rewind_point.file_count == 1 @@ -89,7 +100,7 @@ async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestCo assert rewind.events_removed is not None and rewind.events_removed > 0 assert len(rewind.restored_files) == 1 assert _same_path(rewind.restored_files[0], file_path) - assert not file_path.exists() + assert file_path.read_text(encoding="utf-8") == PREPARED_FILE_CONTENT events = await session.get_events() assert all(str(event.id) != rewind_point.event_id for event in events) diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index 2171e25f2d..8eaecf6244 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -191,7 +191,6 @@ def _apply_inprocess_environment(self) -> None: { "GH_TOKEN": DEFAULT_GITHUB_TOKEN, "GITHUB_TOKEN": DEFAULT_GITHUB_TOKEN, - "COPILOT_CLI_PATH": self.cli_path, "COPILOT_HMAC_KEY": "", "CAPI_HMAC_KEY": "", } @@ -274,11 +273,13 @@ async def configure_for_test(self, test_file: str, test_name: str): if self._proxy: await self._proxy.configure(abs_snapshot_path, self.work_dir) - # Clear temp directories between tests (but leave them in place) - # Use ignore_errors=True / suppress(OSError) to handle race conditions - # where files (e.g., SQLite session-store.db on Windows) may still be - # held open by a background process during cleanup. - for base_dir in (self.home_dir, self.work_dir): + # Keep the in-process runtime's isolated home intact until teardown stops + # the runtime. Removing its open state files on POSIX can leave later tests + # using unlinked database state. + cleanup_dirs = ( + (self.work_dir,) if self._client_inprocess else (self.home_dir, self.work_dir) + ) + for base_dir in cleanup_dirs: base_path = Path(base_dir) base_path.mkdir(parents=True, exist_ok=True) for item in base_path.iterdir(): diff --git a/python/test_cli_download.py b/python/test_cli_download.py index 36952919df..a5a20dce0d 100644 --- a/python/test_cli_download.py +++ b/python/test_cli_download.py @@ -4,6 +4,9 @@ import base64 import hashlib +import io +import os +import tarfile from unittest.mock import patch import pytest @@ -16,6 +19,28 @@ def _integrity(data: bytes, algo: str = "sha512") -> str: return f"{algo}-{base64.b64encode(digest).decode('ascii')}" +def _runtime_package(npm_platform: str) -> bytes: + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + members = { + f"package/prebuilds/{npm_platform}/{wrapper_name}": b"wrapper", + f"package/prebuilds/{npm_platform}/runtime.node": b"runtime", + "package/copilot": b"excluded", + "package/copilot.exe": b"excluded", + f"package/ripgrep/bin/{npm_platform}/rg": b"ripgrep", + "package/definitions/future.json": b"{}", + "package/app.js": b"excluded", + "package/LICENSE.md": b"excluded", + "package/README.md": b"excluded", + } + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w:gz") as archive: + for name, content in members.items(): + info = tarfile.TarInfo(name) + info.size = len(content) + archive.addfile(info, io.BytesIO(content)) + return buffer.getvalue() + + class TestVerifyIntegrity: def test_accepts_matching_checksum(self): data = b"native-library-bytes" @@ -51,3 +76,92 @@ def test_raises_when_integrity_unavailable(self, tmp_path): # The library bytes must never be extracted/written when verification is impossible. extract.assert_not_called() + + +class TestEnsureRuntimeWrapper: + def test_materializes_pair_from_absent_cache_with_stripped_environment( + self, tmp_path, monkeypatch + ): + npm_platform = "win32-x64" if os.name == "nt" else "linux-x64" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + data = _runtime_package(npm_platform) + cache_dir = tmp_path / "cache" + empty_path = tmp_path / "empty-path" + empty_path.mkdir() + assert not cache_dir.exists() + + for name in ( + "COPILOT_CLI_PATH", + "COPILOT_RUNTIME_HOST_COMMAND", + "COPILOT_RUNTIME_PROVIDER_LIB", + ): + monkeypatch.delenv(name, raising=False) + monkeypatch.setenv("PATH", str(empty_path)) + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_npm_platform", return_value=npm_platform), + patch.object(_cli_download, "_should_skip_download", return_value=False), + patch.object(_cli_download, "_fetch_url_bytes", return_value=data), + patch.object( + _cli_download, + "_fetch_runtime_integrity", + return_value=_integrity(data), + ), + ): + wrapper = _cli_download.ensure_runtime_wrapper(version="1.2.3") + + install_dir = cache_dir / "prebuilds" / npm_platform + assert wrapper == str(install_dir / wrapper_name) + assert (install_dir / wrapper_name).read_bytes() == b"wrapper" + assert (install_dir / "runtime.node").read_bytes() == b"runtime" + assert (install_dir / "ripgrep" / "bin" / npm_platform / "rg").read_bytes() == b"ripgrep" + assert (install_dir / "definitions" / "future.json").read_bytes() == b"{}" + assert not (install_dir / "app.js").exists() + assert not (install_dir / "copilot").exists() + assert not (install_dir / "copilot.exe").exists() + assert (install_dir / ".hostless-runtime-assets-v2").is_file() + if os.name != "nt": + assert (install_dir / wrapper_name).stat().st_mode & 0o111 + + def test_rejects_cached_wrapper_without_runtime_node(self, tmp_path): + npm_platform = "win32-x64" if os.name == "nt" else "linux-x64" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + cache_dir = tmp_path / "cache" + install_dir = cache_dir / "prebuilds" / npm_platform + install_dir.mkdir(parents=True) + (install_dir / wrapper_name).write_bytes(b"wrapper") + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_npm_platform", return_value=npm_platform), + ): + with pytest.raises(RuntimeError, match="Incomplete Copilot runtime bundle"): + _cli_download.ensure_runtime_wrapper(version="1.2.3") + + def test_upgrades_pair_only_cache_with_retained_assets(self, tmp_path): + npm_platform = "win32-x64" if os.name == "nt" else "linux-x64" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + cache_dir = tmp_path / "cache" + install_dir = cache_dir / "prebuilds" / npm_platform + install_dir.mkdir(parents=True) + (install_dir / wrapper_name).write_bytes(b"old-wrapper") + (install_dir / "runtime.node").write_bytes(b"old-runtime") + (install_dir / "copilot").write_bytes(b"legacy-sea") + (install_dir / ".hostless-runtime-assets-v1").write_text("1\n", encoding="ascii") + data = _runtime_package(npm_platform) + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_npm_platform", return_value=npm_platform), + patch.object(_cli_download, "_should_skip_download", return_value=False), + patch.object(_cli_download, "_fetch_url_bytes", return_value=data), + patch.object(_cli_download, "_fetch_runtime_integrity", return_value=_integrity(data)), + ): + wrapper = _cli_download.ensure_runtime_wrapper(version="1.2.3") + + assert wrapper == str(install_dir / wrapper_name) + assert (install_dir / wrapper_name).read_bytes() == b"wrapper" + assert not (install_dir / "copilot").exists() + assert (install_dir / ".hostless-runtime-assets-v2").is_file() + assert (install_dir / "ripgrep" / "bin" / npm_platform / "rg").is_file() diff --git a/python/test_client.py b/python/test_client.py index bfcee83db2..cb78db2094 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -59,6 +59,27 @@ def test_inprocess_connection_has_no_child_process_options(): assert not hasattr(connection, "args") +def test_explicit_child_process_path_does_not_require_runtime_bundle(tmp_path): + explicit = tmp_path / "copilot" + connection = RuntimeConnection.for_stdio(path=str(explicit)) + + CopilotClient(connection=connection, env={"PATH": str(tmp_path)}) + + assert connection.path == str(explicit) + + +def test_copilot_cli_path_does_not_require_runtime_bundle(tmp_path): + explicit = tmp_path / "copilot" + connection = RuntimeConnection.for_stdio() + + CopilotClient( + connection=connection, + env={"PATH": str(tmp_path), "COPILOT_CLI_PATH": str(explicit)}, + ) + + assert connection.path == str(explicit) + + class TestBuiltinPluginDirectories: @staticmethod async def _start_client(paths=None): diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0f18a9b159..a28190cd1f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -90,6 +90,21 @@ required-features = ["test-support"] name = "protocol_version_test" required-features = ["test-support"] +[[test]] +name = "extension_launch_provider_test" +required-features = ["test-support"] + +[[test]] +name = "extension_launch_provider_runtime_test" +required-features = ["test-support"] + +[[bin]] +name = "copilot-extension-test-fixture" +path = "tests/fixtures/extension_fixture.rs" +required-features = ["test-support"] +test = false +bench = false + [build-dependencies] base64 = "0.22" dirs = "5" diff --git a/rust/README.md b/rust/README.md index 6114105c25..c05746a30e 100644 --- a/rust/README.md +++ b/rust/README.md @@ -101,8 +101,63 @@ transports. | `env_remove` | `Vec` | Environment variables to remove | | `extra_args` | `Vec` | Extra CLI flags | | `transport` | `Transport` | `Default`, `Stdio`, `InProcess`, `Tcp`, or `External` | +| `extension_launch_provider` | `Option>` | Connection-global extension launch resolver | -With the default `CliProgram::Resolve`, `Client::start()` resolves the CLI in this order: an explicit `CliProgram::Path(path)`, the `COPILOT_CLI_PATH` env var, then the bundled CLI that was embedded at build time. There is no PATH scanning — if you've opted out of bundling (`default-features = false`) you must supply either `CliProgram::Path` or `COPILOT_CLI_PATH`. +With the default `CliProgram::Resolve`, managed stdio and TCP transports resolve an explicit `CliProgram::Path(path)`, `COPILOT_CLI_PATH`, then the bundled `copilot-runtime` wrapper and adjacent `runtime.node`. In-process transport retains its CLI-entrypoint resolution. There is no PATH scanning. + +#### Extension launch provider + +Hosts that own legacy extension process assets can supply a typed, asynchronous +launch resolver: + +```rust,ignore +use std::collections::HashMap; + +use async_trait::async_trait; +use github_copilot_sdk::extension_launch_provider::{ + ExtensionLaunchProfile, ExtensionLaunchProvider, ExtensionLaunchProviderResolveRequest, + ExtensionLaunchProviderResolveResult, +}; +use github_copilot_sdk::{Client, ClientOptions, Result}; + +struct AppExtensionLaunchProvider; + +#[async_trait] +impl ExtensionLaunchProvider for AppExtensionLaunchProvider { + async fn resolve( + &self, + request: ExtensionLaunchProviderResolveRequest, + ) -> Result { + Ok(ExtensionLaunchProviderResolveResult { + launch: Some(ExtensionLaunchProfile { + executable: "/app/copilot".to_string(), + args: vec!["/app/preloads/extension_bootstrap.mjs".to_string()], + env: HashMap::from([ + ("COPILOT_AUTO_UPDATE".to_string(), "false".to_string()), + ("EXTENSION_PATH".to_string(), request.module_path), + ]), + }), + }) + } +} + +let client = Client::start( + ClientOptions::new().with_extension_launch_provider(AppExtensionLaunchProvider), +).await?; +``` + +`Client::start` registers the provider before returning, and reverse requests +are routed at the connection level rather than through a session. The SDK +forwards the returned executable, arguments, and environment unchanged; it +does not discover or bundle an executable or bootstrap. The runtime owns and +overrides `COPILOT_SDK_PATH`, `SESSION_ID`, and +`COPILOT_EXTENSION_PARENT_PID`. + +`COPILOT_CLI_DIST_DIR` is only appropriate when the host supplies a complete +CLI distribution containing `index.js` and its matching preloads. When the +executable is a version-matched standalone Copilot binary, omit that variable +and set `COPILOT_AUTO_UPDATE=false` so its embedded distribution remains +selected. ### Session @@ -826,6 +881,7 @@ none of them are scheduled for removal. | File | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------- | | `lib.rs` | `Client`, `ClientOptions`, `CliProgram`, `Transport`, `Error` | +| `extension_launch_provider.rs` | Connection-global `ExtensionLaunchProvider` trait and launch profile DTOs | | `session.rs` | `Session` struct, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session` | | `subscription.rs` | `EventSubscription` / `LifecycleSubscription` (`Stream`-able observer handles for `subscribe()` / `subscribe_lifecycle()`) | | `handler.rs` | `PermissionHandler`, `ElicitationHandler`, `UserInputHandler`, `ExitPlanModeHandler`, `AutoModeSwitchHandler` traits; `ApproveAllHandler`, `DenyAllHandler` | @@ -835,13 +891,15 @@ none of them are scheduled for removal. | `types.rs` | CLI protocol types (`SessionId`, `SessionEvent`, `SessionConfig`, `Tool`, etc.) | | `resolve.rs` | Bundled-CLI resolution (`copilot_binary`) | | `embeddedcli.rs` | Embedded CLI extraction (gated on the default `bundled-cli` feature) | -| `router.rs` | Internal per-session event demux | +| `router.rs` | Internal connection-global request dispatch and per-session event demux | | `jsonrpc.rs` | Internal Content-Length framed JSON-RPC transport | -## Embedded CLI +## Bundled runtime artifacts The SDK provisions its runtime at build time. By default the `bundled-cli` -feature embeds the verified child-process runtime in your compiled crate. +feature embeds the verified `copilot-runtime` wrapper and adjacent +`runtime.node` in your compiled crate. The compatible CLI artifact remains +available separately for `install_bundled_cli` and in-process hosting. Enable `bundled-in-process` to additionally embed the native runtime library and use `Transport::InProcess`: @@ -859,15 +917,11 @@ For builds that prefer a smaller artifact, disable the `bundled-cli` feature: github-copilot-sdk = { version = "0.1", default-features = false } ``` -> **You become responsible for supplying the CLI at runtime.** With -> `bundled-cli` disabled, the produced binary does not contain the CLI -> and will not search the system for one. You must point it at a -> compatible CLI via [`CliProgram::Path`] (on `ClientOptions`) or the -> `COPILOT_CLI_PATH` environment variable, and you are responsible for -> guaranteeing the supplied CLI version is compatible with this SDK -> release. Do **not** assume that whatever CLI happens to be installed -> on the target system will work — the SDK and CLI are versioned -> together. +> **You become responsible for supplying the runtime at deployment.** With +> `bundled-cli` disabled, the produced binary does not contain these artifacts +> and will not search the system for them. For managed child-process transports, +> supply a compatible wrapper pair via an explicit [`CliProgram::Path`]. +> `COPILOT_CLI_PATH` remains a direct program override. > > **Convenience on the build machine only.** As a special case, > `build.rs` downloads and integrity-verifies the compatible CLI version and @@ -876,8 +930,8 @@ github-copilot-sdk = { version = "0.1", default-features = false } > makes local development and CI ergonomic, but it does **not** carry > over when you copy the built binary to another machine — distributed > builds (release artifacts, signed installers, container images, etc.) -> must either keep `bundled-cli` enabled or ship the CLI alongside and -> set `CliProgram::Path` / `COPILOT_CLI_PATH`. +> must either keep `bundled-cli` enabled or ship the runtime pair and set +> `CliProgram::Path`. ### How it works @@ -890,17 +944,17 @@ github-copilot-sdk = { version = "0.1", default-features = false } 2. **Build time:** `build.rs` downloads the platform-specific npm package and verifies its `sha512` integrity against the lockfile or publish snapshot. Then: - - **`bundled-cli` on (default):** creates and embeds a minimal archive containing only the CLI executable. - - **`bundled-in-process` on:** the minimal archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`); no other npm package files are embedded. - - **`bundled-cli` off:** extracts the binary directly into the platform cache (staging file + atomic rename), idempotent across rebuilds. If the extracted binary is already present at the expected path, the download is skipped entirely — the extracted binary *is* the cache. + - **`bundled-cli` on (default):** creates and embeds a minimal archive containing the CLI executable, `copilot-runtime[.exe]`, and `runtime.node`. + - **`bundled-in-process` on:** the archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`). + - **`bundled-cli` off:** extracts the same artifacts directly into the platform cache using staging files and atomic renames. -3. **Runtime:** in both modes the binary lives at: +3. **Runtime:** in both modes the artifacts share one versioned directory: | OS | Path | |----|------| - | macOS | `~/Library/Caches/github-copilot-sdk/cli//copilot` | - | Linux | `${XDG_CACHE_HOME:-~/.cache}/github-copilot-sdk/cli//copilot` | - | Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\copilot.exe` | + | macOS | `~/Library/Caches/github-copilot-sdk/cli//` | + | Linux | `${XDG_CACHE_HOME:-~/.cache}/github-copilot-sdk/cli//` | + | Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\` | Old version directories accumulate in siblings; clean them up at your leisure. @@ -929,18 +983,20 @@ COPILOT_CLI_EXTRACT_DIR = { value = "vendor/copilot", relative = true, force = t ### Skipping the bundle entirely -Set `COPILOT_SKIP_CLI_DOWNLOAD=1` at build time to disable the entire download / bundle / cache mechanism — `build.rs` returns immediately without touching the network. Use this when you always supply the CLI at runtime via `ClientOptions::program = CliProgram::Path(...)` or `COPILOT_CLI_PATH`. Works regardless of the `bundled-cli` feature state; runtime resolution falls through to `Error::BinaryNotFound` unless one of those explicit sources resolves. +Set `COPILOT_SKIP_CLI_DOWNLOAD=1` at build time to disable the entire download / bundle / cache mechanism — `build.rs` returns immediately without touching the network. Use this when you always supply the managed runtime via `ClientOptions::program = CliProgram::Path(...)`. Works regardless of the `bundled-cli` feature state; runtime resolution falls through to `Error::BinaryNotFound` unless an applicable explicit source resolves. ### Resolution priority -`Client::start` resolves the CLI in this order: +For managed child-process transports, `Client::start` resolves the program in this order: 1. Explicit `CliProgram::Path(path)` on `ClientOptions::program`. 2. `COPILOT_CLI_PATH` environment variable, if it points at a real file. -3. **`bundled-cli` on:** the embedded archive, lazily extracted on first call. -4. **`bundled-cli` off:** the build-time-extracted binary in the per-user cache, located by recomputing the convention from `COPILOT_SDK_CLI_VERSION` + OS + optional `COPILOT_CLI_EXTRACT_DIR`. +3. **`bundled-cli` on:** the embedded wrapper pair, lazily extracted on first call. +4. **`bundled-cli` off:** the build-time-extracted wrapper pair in the per-user cache. -There is no PATH scanning. If none of the above resolves, `Client::start` returns `Error::BinaryNotFound`. +In-process transport resolves the compatible CLI artifact from +`COPILOT_CLI_PATH`, the embedded archive, or the build-time cache. There is no +PATH scanning. ### Reaching the bundled binary without a `Client` @@ -960,12 +1016,24 @@ if HAS_BUNDLED_CLI { } ``` -This returns the same path `Client::start` would resolve to for -`CliProgram::Resolve` with no `COPILOT_CLI_PATH` override and no -`ClientOptions::bundled_cli_extract_dir` configured. It returns `None` -when `bundled-cli` is off or the target is unsupported, and (unlike the -full resolver) does not fall back to the build-time-extracted dev-cache -path. +This returns the bundled CLI artifact, preserving the public API's original +meaning. Managed child-process transports resolve `copilot-runtime` instead. +The function returns `None` when `bundled-cli` is off or the target is +unsupported and does not fall back to the build-time extraction cache. + +Use [`install_bundled_runtime`] when a health check or intermediate launcher +needs the managed runtime executable: + +```rust,no_run +use github_copilot_sdk::install_bundled_runtime; + +if let Some(path) = install_bundled_runtime() { + println!("bundled runtime at {}", path.display()); +} +``` + +This extracts `copilot-runtime` together with adjacent `runtime.node`, then +returns the wrapper path. ### Download cache (build-time, embed mode) @@ -979,8 +1047,8 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64` | Feature | Default | Description | | ------- | ------- | ----------- | -| `bundled-cli` | ✓ | Embeds only the CLI executable. Disable via `default-features = false` when supplying the CLI via `CliProgram::Path` or `COPILOT_CLI_PATH`. | -| `bundled-in-process` | — | Enables `Transport::InProcess`, implies `bundled-cli`, and additionally embeds only the platform-native runtime library. | +| `bundled-cli` | ✓ | Embeds the managed wrapper pair and compatible CLI artifact. Disable via `default-features = false` when supplying the runtime explicitly. | +| `bundled-in-process` | — | Enables `Transport::InProcess`, implies `bundled-cli`, and additionally embeds the platform-native runtime library. | | `derive` | — | `schema_for::()` for generating JSON Schema from Rust types (adds `schemars`). | ```toml diff --git a/rust/build.rs b/rust/build.rs index d04cf2870b..c01464bb4a 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -1,11 +1,6 @@ -#[cfg(feature = "bundled-in-process")] #[path = "build/in_process.rs"] mod implementation; -#[cfg(not(feature = "bundled-in-process"))] -#[path = "build/out_of_process.rs"] -mod implementation; - fn main() { implementation::main(); } diff --git a/rust/build/in_process.rs b/rust/build/in_process.rs index 5826fbfa76..f1e947b27a 100644 --- a/rust/build/in_process.rs +++ b/rust/build/in_process.rs @@ -40,7 +40,7 @@ pub(crate) fn main() { // path source resolves first. if std::env::var_os("COPILOT_SKIP_CLI_DOWNLOAD").is_some() { println!( - "cargo:warning=COPILOT_SKIP_CLI_DOWNLOAD is set — skipping CLI download/bundle/cache" + "cargo:warning=COPILOT_SKIP_CLI_DOWNLOAD is set — skipping runtime download/bundle/cache" ); return; } @@ -95,38 +95,61 @@ pub(crate) fn main() { if std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_some() { let archive = cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir); - verify_binary_present_in_archive(&archive, platform.binary_name, &archive_name); + verify_runtime_package(&archive, platform, &archive_name); emit_embedded(out, &archive, platform, include_runtime); println!("cargo:rustc-cfg=has_bundled_cli"); } else { - // With `bundled-cli` off the extracted binary *is* the cache. - // Skip the upstream download entirely when it already exists at - // the expected path. No two separate caches. + // With `bundled-cli` off the extracted runtime pair *is* the cache. + // Skip the upstream download entirely when both files already exist. // - // Runtime resolution (see `src/resolve.rs::extracted_cli_path`) + // Runtime resolution (see `src/resolve.rs::extracted_program`) // recomputes this same path from `COPILOT_SDK_CLI_VERSION` + the // OS-derived binary name + optional `COPILOT_CLI_EXTRACT_DIR`, // so we don't bake an absolute path into the crate. let install_dir = extracted_install_dir(&version); - let final_path = install_dir.join(platform.binary_name); - - // Invalidate build.rs whenever the cached binary disappears (cache GC, - // manual rm, OS reset, switching extract dir). Without this, cargo + let required_paths = [ + install_dir.join(platform.runtime_wrapper_name()), + install_dir.join("runtime.node"), + install_dir.join(".hostless-runtime-assets-v1"), + ]; + let expected_marker = format!("{version}\n{expected_integrity}\n"); + + // Invalidate build.rs whenever either cached artifact disappears (cache + // GC, manual rm, OS reset, switching extract dir). Without this, cargo // replays the saved `has_extracted_cli` cfg from its build-script // output cache even when the file is gone, and runtime resolution // fails with BinaryNotFound. - println!("cargo:rerun-if-changed={}", final_path.display()); + for path in &required_paths { + println!("cargo:rerun-if-changed={}", path.display()); + } - if !final_path.is_file() { + let cache_is_current = required_paths.iter().all(|path| path.is_file()) + && std::fs::read_to_string(&required_paths[2]).ok().as_deref() + == Some(expected_marker.as_str()); + if !cache_is_current { + if install_dir.exists() { + std::fs::remove_dir_all(&install_dir).unwrap_or_else(|e| { + panic!( + "failed to clear stale runtime bundle {}: {e}", + install_dir.display() + ) + }); + } let archive = cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir); - verify_binary_present_in_archive(&archive, platform.binary_name, &archive_name); - extract_to_cache(&archive, &install_dir, platform); + verify_runtime_package(&archive, platform, &archive_name); + extract_to_cache( + &archive, + &install_dir, + platform, + include_runtime, + &expected_marker, + ); } // Re-check after potential download+extract above; not an `else` // because we need to verify the extraction actually produced the file. - if final_path.is_file() { + if required_paths.iter().all(|path| path.is_file()) { println!("cargo:rustc-cfg=has_extracted_cli"); } } @@ -176,19 +199,8 @@ fn build_embedded_archive(package: &[u8], platform: Platform, include_runtime: b .mtime(0) .write(Vec::new(), flate2::Compression::default()); let mut archive = tar::Builder::new(encoder); - append_archive_file( - &mut archive, - platform.binary_name, - &extract_binary_bytes(package, platform), - 0o755, - ); + let runtime = append_hostless_runtime_tree(&mut archive, package, platform); if include_runtime { - let runtime = extract_runtime_library_bytes(package).unwrap_or_else(|| { - panic!( - "package `{}` does not contain the native runtime library required by the `bundled-in-process` feature", - platform.package_name - ) - }); append_archive_file( &mut archive, platform.runtime_library_name(), @@ -204,6 +216,103 @@ fn build_embedded_archive(package: &[u8], platform: Platform, include_runtime: b .expect("failed to compress minimal embedded CLI archive") } +fn append_hostless_runtime_tree( + archive: &mut tar::Builder, + package: &[u8], + platform: Platform, +) -> Vec { + let decoder = flate2::read::GzDecoder::new(package); + let mut source = tar::Archive::new(decoder); + let mut runtime = None; + for entry in source + .entries() + .unwrap_or_else(|e| panic!("failed to read npm package entries: {e}")) + { + let mut entry = entry.unwrap_or_else(|e| panic!("failed to read npm package entry: {e}")); + if !entry.header().entry_type().is_file() { + continue; + } + let source_path = entry + .path() + .unwrap_or_else(|e| panic!("failed to read npm package path: {e}")); + let Some(destination) = hostless_runtime_path(&source_path.to_string_lossy(), platform) + else { + continue; + }; + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .unwrap_or_else(|e| panic!("failed to read npm package entry bytes: {e}")); + let mode = entry.header().mode().unwrap_or(0o644); + if destination == Path::new("runtime.node") { + runtime = Some(bytes.clone()); + } + append_archive_file( + archive, + destination + .to_str() + .expect("npm package paths are valid UTF-8"), + &bytes, + mode, + ); + } + runtime.unwrap_or_else(|| { + panic!( + "package `{}` does not contain prebuilds//runtime.node", + platform.package_name + ) + }) +} + +fn hostless_runtime_path(source: &str, platform: Platform) -> Option { + let relative = source.strip_prefix("package/")?; + let parts: Vec<&str> = relative.split('/').collect(); + if parts.iter().any(|part| part.is_empty() || *part == "..") { + return None; + } + let top_level = *parts.first()?; + let file_name = *parts.last()?; + const EXCLUDED_TOP_LEVEL: &[&str] = &[ + "app.js", + "assets", + "changelog.json", + "copilot-sdk", + "foundry-local-sdk", + "index.js", + "LICENSE.md", + "napi-oop-runtime", + "npm-loader.js", + "package.json", + "preloads", + "pvrecorder", + "queries", + "README.md", + "sdk", + "sea-loader.js", + "webview", + ]; + if EXCLUDED_TOP_LEVEL.contains(&top_level) + || (top_level.starts_with("tree-sitter") && top_level.ends_with(".wasm")) + || (top_level.starts_with("voice-") && top_level.ends_with(".js")) + || file_name == "cli-native.node" + || parts.contains(&"mediaremote-adapter") + || file_name.starts_with("copilot-runtime-bin") + { + return None; + } + if top_level == "prebuilds" { + let npm_platform = platform + .package_name + .strip_prefix("copilot-") + .expect("platform package name has copilot- prefix"); + if parts.get(1) != Some(&npm_platform) || parts.len() < 3 { + return None; + } + return Some(parts[2..].iter().copied().collect()); + } + Some(parts.iter().copied().collect()) +} + fn append_archive_file( archive: &mut tar::Builder, path: &str, @@ -315,6 +424,14 @@ struct Platform { } impl Platform { + fn runtime_wrapper_name(&self) -> &'static str { + if self.package_name.contains("win32") { + "copilot-runtime.exe" + } else { + "copilot-runtime" + } + } + fn runtime_library_name(&self) -> &'static str { if self.package_name.contains("win32") { "copilot_runtime.dll" @@ -368,8 +485,8 @@ fn target_platform() -> Option { } } -/// Write the single binary entry from `archive` to -/// `/` and return the resulting path. +/// Write the runtime wrapper pair from `archive` to `install_dir` and return +/// the wrapper path. /// Idempotent — returns the existing path if a previous build already /// populated the target. /// @@ -378,15 +495,13 @@ fn target_platform() -> Option { /// binary. `fs::rename` for files is atomic on both Unix and Windows /// (Windows uses `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING`); for /// directories it is not, which is why we stage at file granularity. -fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> PathBuf { - let final_path = install_dir.join(platform.binary_name); - - // Caller already gated on `final_path.is_file()`; this is a safety - // net for any future caller that forgets. - if final_path.is_file() { - return final_path; - } - +fn extract_to_cache( + archive: &[u8], + install_dir: &Path, + platform: Platform, + include_runtime: bool, + marker: &str, +) -> PathBuf { std::fs::create_dir_all(install_dir).unwrap_or_else(|e| { panic!( "failed to create install dir {}: {e}", @@ -394,8 +509,90 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P ) }); - let bytes = extract_binary_bytes(archive, platform); + let decoder = flate2::read::GzDecoder::new(archive); + let mut source = tar::Archive::new(decoder); + let mut runtime = None; + for entry in source + .entries() + .unwrap_or_else(|e| panic!("failed to read npm package entries: {e}")) + { + let mut entry = entry.unwrap_or_else(|e| panic!("failed to read npm package entry: {e}")); + if !entry.header().entry_type().is_file() { + continue; + } + let source_path = entry + .path() + .unwrap_or_else(|e| panic!("failed to read npm package path: {e}")); + let Some(destination) = hostless_runtime_path(&source_path.to_string_lossy(), platform) + else { + continue; + }; + if destination == Path::new(platform.binary_name) { + continue; + } + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .unwrap_or_else(|e| panic!("failed to read npm package entry bytes: {e}")); + let executable = entry.header().mode().unwrap_or(0o644) & 0o111 != 0; + if destination == Path::new("runtime.node") { + runtime = Some(bytes.clone()); + } + install_cached_file_path(install_dir, &destination, &bytes, executable); + } + let runtime = runtime.expect("verified runtime.node is present"); + if include_runtime { + install_cached_file( + install_dir, + platform.runtime_library_name(), + &runtime, + false, + ); + } + install_cached_file( + install_dir, + ".hostless-runtime-assets-v1", + marker.as_bytes(), + false, + ); + + let final_path = install_dir.join(platform.runtime_wrapper_name()); + println!( + "cargo:warning=Extracted Copilot runtime bundle to {}", + install_dir.display() + ); + final_path +} + +fn install_cached_file(install_dir: &Path, file_name: &str, bytes: &[u8], executable: bool) { + install_cached_file_path(install_dir, Path::new(file_name), bytes, executable); +} +fn install_cached_file_path( + install_dir: &Path, + relative_path: &Path, + bytes: &[u8], + executable: bool, +) { + assert!( + !relative_path.is_absolute() + && !relative_path.components().any(|component| { + matches!( + component, + std::path::Component::Prefix(_) + | std::path::Component::RootDir + | std::path::Component::ParentDir + ) + }), + "unsafe runtime package path: {}", + relative_path.display() + ); + let final_path = install_dir.join(relative_path); + if final_path.is_file() { + return; + } + std::fs::create_dir_all(final_path.parent().expect("runtime asset has parent")) + .unwrap_or_else(|e| panic!("failed to create runtime asset directory: {e}")); // Staging file is a sibling of the final binary so the rename stays // on the same filesystem (cross-fs rename is not atomic). PID + nanos // disambiguate concurrent builds racing on the same cache. @@ -405,7 +602,10 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P .unwrap_or(0); let staging_path = install_dir.join(format!( ".{}.staging-{}-{nanos}", - platform.binary_name, + relative_path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("runtime-asset"), std::process::id(), )); @@ -418,7 +618,7 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P ); }); - if let Err(e) = f.write_all(&bytes) { + if let Err(e) = f.write_all(bytes) { let _ = std::fs::remove_file(&staging_path); panic!( "failed to write staging file {}: {e}", @@ -427,7 +627,7 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P } #[cfg(unix)] - { + if executable { use std::os::unix::fs::PermissionsExt; if let Err(e) = f.set_permissions(std::fs::Permissions::from_mode(0o755)) { let _ = std::fs::remove_file(&staging_path); @@ -472,32 +672,6 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P final_path.display() ); } - - // Surface where the binary landed so contributors can find it. Quiet - // on the hot path: the caller's `is_file()` short-circuit (and the - // safety net at the top of this function) means this only fires on a - // true cache miss. - println!( - "cargo:warning=Extracted Copilot CLI to {}", - final_path.display() - ); - - final_path -} - -fn extract_runtime_library_bytes(archive: &[u8]) -> Option> { - let gz = flate2::read::GzDecoder::new(archive); - let mut tar = tar::Archive::new(gz); - for entry in tar.entries().ok()? { - let mut entry = entry.ok()?; - let name = entry.path().ok()?.to_string_lossy().into_owned(); - if name == "runtime.node" || name.ends_with("/runtime.node") { - let mut bytes = Vec::with_capacity(entry.size() as usize); - entry.read_to_end(&mut bytes).ok()?; - return Some(bytes); - } - } - None } /// Replace characters outside `[a-zA-Z0-9._-]` with `_` so the version @@ -514,37 +688,6 @@ fn sanitize_version(version: &str) -> String { .collect() } -/// Extract the single `binary_name` entry from the npm package archive. Reused -/// between embed mode's `verify_binary_present_in_archive` and the -/// `extract_to_cache` path used when `bundled-cli` is off. Panics if the -/// entry isn't found — callers have already invoked -/// `verify_binary_present_in_archive`. -fn extract_binary_bytes(archive: &[u8], platform: Platform) -> Vec { - let gz = flate2::read::GzDecoder::new(archive); - let mut tar = tar::Archive::new(gz); - for entry in tar - .entries() - .unwrap_or_else(|e| panic!("failed to read tar entries: {e}")) - { - let mut entry = entry.unwrap_or_else(|e| panic!("failed to read tar entry: {e}")); - let path = entry - .path() - .unwrap_or_else(|e| panic!("failed to read tar entry path: {e}")); - let name = path.to_string_lossy().into_owned(); - if name == platform.binary_name || name.ends_with(&format!("/{}", platform.binary_name)) { - let mut bytes = Vec::with_capacity(entry.size() as usize); - entry - .read_to_end(&mut bytes) - .unwrap_or_else(|e| panic!("failed to read tar entry bytes: {e}")); - return bytes; - } - } - panic!( - "binary `{}` not found in package `{}`", - platform.binary_name, platform.package_name - ); -} - /// Read a file from the download cache, or download it (with retries) and save /// to cache. Verifies npm integrity on every path. Evicts stale/corrupt cache entries /// automatically. Cache I/O failures are treated as cache misses — they never @@ -682,15 +825,17 @@ fn try_download(url: &str) -> Result, DownloadError> { } } -/// Walks the downloaded archive at build time to confirm an entry matching -/// `binary_name` exists. Panics with a clear message if not. -fn verify_binary_present_in_archive(archive: &[u8], binary_name: &str, package_name: &str) { - let found = archive_contains_tar_entry(archive, binary_name); - if !found { +fn verify_runtime_package(archive: &[u8], platform: Platform, package_name: &str) { + for file_name in [ + platform.binary_name, + "runtime.node", + platform.runtime_wrapper_name(), + ] { + if archive_contains_tar_entry(archive, file_name) { + continue; + } panic!( - "Copilot CLI package `{package_name}` does not contain an entry named `{binary_name}`. \ - The package layout may have changed; runtime extraction would fail. \ - Update `verify_binary_present_in_archive` in build.rs and the matching `extract_binary` in src/embeddedcli.rs." + "Copilot runtime package `{package_name}` does not contain an entry named `{file_name}`" ); } } diff --git a/rust/src/embeddedcli.rs b/rust/src/embeddedcli.rs index 40900a4d22..3cc527a2e2 100644 --- a/rust/src/embeddedcli.rs +++ b/rust/src/embeddedcli.rs @@ -3,10 +3,10 @@ //! feature set). //! //! Normal builds embed the platform release archive from GitHub Releases. -//! Builds with `bundled-in-process` instead embed a minimal archive from the -//! platform npm package containing the CLI executable and native runtime -//! library. Extraction to a real on-disk path is deferred until the first call -//! to [`path`] / [`install_at`]. +//! Builds with `bundled-in-process` instead embed a filtered archive from the +//! platform npm package containing the CLI executable, runtime wrapper, native +//! runtime artifacts, and auxiliary runtime assets. Extraction to a real +//! on-disk path is deferred until the relevant installer is called. //! //! The embedded bytes are part of the consumer's signed binary and therefore //! trusted *as the source of truth* — but the bytes that land on disk are not. @@ -28,7 +28,7 @@ // off but still needs to exercise them. #[cfg(any(has_bundled_cli, test))] use std::fs; -#[cfg(all(has_bundled_cli, any(feature = "bundled-in-process", not(windows))))] +#[cfg(has_bundled_cli)] use std::io::Read; #[cfg(any(has_bundled_cli, test))] use std::io::Write; @@ -65,9 +65,19 @@ const CLI_VERSION: &str = env!("COPILOT_SDK_CLI_VERSION"); const CLI_BINARY_NAME: &str = "copilot.exe"; #[cfg(all(has_bundled_cli, not(windows)))] const CLI_BINARY_NAME: &str = "copilot"; +#[cfg(all(has_bundled_cli, windows))] +const RUNTIME_BINARY_NAME: &str = "copilot-runtime.exe"; +#[cfg(all(has_bundled_cli, not(windows)))] +const RUNTIME_BINARY_NAME: &str = "copilot-runtime"; +#[cfg(has_bundled_cli)] +const RUNTIME_NODE_NAME: &str = "runtime.node"; +#[cfg(has_bundled_cli)] +const RUNTIME_VERSION_MARKER: &str = ".copilot-runtime-version"; #[cfg(feature = "bundled-cli")] static INSTALLED_PATH: OnceLock> = OnceLock::new(); +#[cfg(feature = "bundled-cli")] +static INSTALLED_RUNTIME_PATH: OnceLock> = OnceLock::new(); /// Returns the path to the installed CLI binary, lazily extracting the /// embedded archive on first call. @@ -91,7 +101,7 @@ pub(crate) fn path() -> Option { #[cfg(has_bundled_cli)] { let dir = default_install_dir(CLI_VERSION); - match install(&dir, build_time::CLI_ARCHIVE) { + match install_cli_bundle(&dir, build_time::CLI_ARCHIVE) { Ok(path) => { info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed"); return Some(path); @@ -119,7 +129,7 @@ pub(crate) fn path() -> Option { pub(crate) fn install_at(extract_dir: &Path) -> Option { #[cfg(has_bundled_cli)] { - match install(extract_dir, build_time::CLI_ARCHIVE) { + match install_cli_bundle(extract_dir, build_time::CLI_ARCHIVE) { Ok(path) => { info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed"); return Some(path); @@ -136,6 +146,93 @@ pub(crate) fn install_at(extract_dir: &Path) -> Option { None } +/// Returns the path to the bundled runtime wrapper, extracting the wrapper and +/// adjacent `runtime.node` on first call. +#[cfg(feature = "bundled-cli")] +pub(crate) fn runtime_path() -> Option { + INSTALLED_RUNTIME_PATH + .get_or_init(|| { + #[cfg(has_bundled_cli)] + { + let dir = default_install_dir(CLI_VERSION); + match install_runtime(&dir, build_time::CLI_ARCHIVE) { + Ok(path) => { + info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed"); + return Some(path); + } + Err(e) => { + warn!(error = %e, "embedded runtime installation failed"); + } + } + } + None + }) + .clone() +} + +/// Installs the bundled runtime wrapper and adjacent `runtime.node` into a +/// caller-specified directory. +#[cfg(feature = "bundled-cli")] +pub(crate) fn install_runtime_at(extract_dir: &Path) -> Option { + #[cfg(has_bundled_cli)] + { + let install_dir = match runtime_install_dir(extract_dir, CLI_VERSION) { + Ok(dir) => dir, + Err(e) => { + warn!(error = %e, "embedded runtime install directory selection failed"); + return None; + } + }; + match install_runtime(&install_dir, build_time::CLI_ARCHIVE) { + Ok(path) => { + info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed"); + return Some(path); + } + Err(e) => { + warn!(error = %e, "embedded runtime installation failed"); + } + } + } + #[cfg(not(has_bundled_cli))] + { + let _ = extract_dir; + } + None +} + +#[cfg(has_bundled_cli)] +fn runtime_install_dir(base_dir: &Path, version: &str) -> Result { + fs::create_dir_all(base_dir) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?; + let marker = base_dir.join(RUNTIME_VERSION_MARKER); + match fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&marker) + { + Ok(mut file) => { + if let Err(error) = file + .write_all(version.as_bytes()) + .and_then(|()| file.sync_all()) + { + drop(file); + let _ = fs::remove_file(&marker); + return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, error)); + } + Ok(base_dir.to_path_buf()) + } + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + let installed_version = fs::read_to_string(marker).unwrap_or_default(); + if installed_version == version { + Ok(base_dir.to_path_buf()) + } else { + Ok(base_dir.join(version)) + } + } + Err(error) => Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, error)), + } +} + #[cfg(has_bundled_cli)] fn default_install_dir(version: &str) -> PathBuf { let cache = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); @@ -168,34 +265,151 @@ const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.dylib"; const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.so"; #[cfg(has_bundled_cli)] -fn install(install_dir: &Path, archive: &[u8]) -> Result { - let final_path = install_cli(install_dir, archive)?; +fn install_cli_bundle(install_dir: &Path, archive: &[u8]) -> Result { + install_cli(install_dir, archive)?; + install_hostless_assets(install_dir, archive)?; #[cfg(feature = "bundled-in-process")] { install_runtime_library(install_dir, archive)?; } - Ok(final_path) + Ok(install_dir.join(CLI_BINARY_NAME)) +} + +#[cfg(has_bundled_cli)] +fn install_runtime(install_dir: &Path, archive: &[u8]) -> Result { + fs::create_dir_all(install_dir) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?; + install_hostless_assets(install_dir, archive)?; + install_runtime_pair(install_dir, archive)?; + Ok(install_dir.join(RUNTIME_BINARY_NAME)) +} + +#[cfg(has_bundled_cli)] +fn install_hostless_assets(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> { + let gz = flate2::read::GzDecoder::new(archive); + let mut tar = tar::Archive::new(gz); + for entry in tar + .entries() + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))? + { + let mut entry = + entry.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?; + if !entry.header().entry_type().is_file() { + continue; + } + let path = entry + .path() + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))? + .into_owned(); + let file_name = path.file_name().and_then(|name| name.to_str()); + if path == Path::new(CLI_BINARY_NAME) + || matches!( + file_name, + Some("copilot_runtime.dll") + | Some("libcopilot_runtime.dylib") + | Some("libcopilot_runtime.so") + ) + { + continue; + } + if path.is_absolute() + || path.components().any(|component| { + matches!( + component, + std::path::Component::Prefix(_) + | std::path::Component::RootDir + | std::path::Component::ParentDir + ) + }) + { + return Err(EmbeddedCliError::with_message( + EmbeddedCliErrorKind::Archive, + format!("unsafe embedded runtime asset path: {}", path.display()), + )); + } + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?; + let target = install_dir.join(&path); + if fs::read(&target) + .map(|installed| installed == bytes) + .unwrap_or(false) + { + continue; + } + let parent = target.parent().ok_or_else(|| { + EmbeddedCliError::with_message( + EmbeddedCliErrorKind::Archive, + format!("embedded runtime asset has no parent: {}", path.display()), + ) + })?; + fs::create_dir_all(parent) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?; + let tmp = write_temp_file(parent, &bytes)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = entry.header().mode().unwrap_or(0o644) & 0o777; + fs::set_permissions(&tmp, fs::Permissions::from_mode(mode)) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?; + } + if let Err(error) = publish(&tmp, &target) { + let _ = fs::remove_file(&tmp); + return Err(error); + } + } + Ok(()) +} + +#[cfg(has_bundled_cli)] +fn install_runtime_pair(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> { + install_adjacent_file(install_dir, archive, RUNTIME_NODE_NAME, "runtime.node")?; + install_adjacent_file( + install_dir, + archive, + RUNTIME_BINARY_NAME, + "copilot runtime wrapper", + ) } #[cfg(all(has_bundled_cli, feature = "bundled-in-process"))] fn install_runtime_library(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> { - let target = install_dir.join(RUNTIME_LIBRARY_NAME); - if fs::metadata(&target).map(|m| m.len() > 0).unwrap_or(false) { - return Ok(()); - } - let bytes = extract_binary(archive, RUNTIME_LIBRARY_NAME)?; + install_adjacent_file( + install_dir, + archive, + RUNTIME_LIBRARY_NAME, + "in-process FFI runtime library", + ) +} + +#[cfg(has_bundled_cli)] +fn install_adjacent_file( + install_dir: &Path, + archive: &[u8], + file_name: &str, + label: &str, +) -> Result<(), EmbeddedCliError> { + let target = install_dir.join(file_name); + let bytes = extract_binary(archive, file_name)?; if bytes.is_empty() { return Err(EmbeddedCliError::with_message( EmbeddedCliErrorKind::Verification, - "embedded runtime library is empty", + format!("embedded {label} is empty"), )); } + if fs::read(&target) + .map(|installed| installed == bytes) + .unwrap_or(false) + { + return Ok(()); + } let tmp = write_temp_file(install_dir, &bytes)?; if let Err(e) = publish(&tmp, &target) { let _ = fs::remove_file(&tmp); return Err(e); } - tracing::debug!(path = %target.display(), "in-process FFI runtime library installed"); + tracing::debug!(path = %target.display(), %label, "embedded runtime artifact installed"); Ok(()) } @@ -480,7 +694,7 @@ fn read_marker_len(marker_path: &Path) -> Option { .ok() } -#[cfg(all(has_bundled_cli, any(feature = "bundled-in-process", not(windows))))] +#[cfg(has_bundled_cli)] fn extract_binary(archive: &[u8], binary_name: &str) -> Result, EmbeddedCliError> { let gz = flate2::read::GzDecoder::new(archive); let mut tar = tar::Archive::new(gz); @@ -505,26 +719,6 @@ fn extract_binary(archive: &[u8], binary_name: &str) -> Result, Embedded Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into()) } -#[cfg(all(has_bundled_cli, not(feature = "bundled-in-process"), windows))] -fn extract_binary(archive: &[u8], binary_name: &str) -> Result, EmbeddedCliError> { - let cursor = std::io::Cursor::new(archive); - let mut zip = zip::ZipArchive::new(cursor) - .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Zip, e))?; - for i in 0..zip.len() { - let mut entry = zip - .by_index(i) - .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Zip, e))?; - let name = entry.name().to_string(); - if name == binary_name || name.ends_with(&format!("/{binary_name}")) { - let mut bytes = Vec::with_capacity(entry.size() as usize); - std::io::copy(&mut entry, &mut bytes) - .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?; - return Ok(bytes); - } - } - Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into()) -} - #[cfg(has_bundled_cli)] fn sanitize_version(version: &str) -> String { version @@ -541,10 +735,7 @@ fn sanitize_version(version: &str) -> String { #[allow(dead_code)] enum EmbeddedCliErrorKind { CreateDir, - #[cfg(any(feature = "bundled-in-process", not(windows)))] Archive, - #[cfg(all(not(feature = "bundled-in-process"), windows))] - Zip, BinaryNotFoundInArchive, Io, /// Atomically renaming the staged temp file onto the final path failed. @@ -561,10 +752,7 @@ impl std::fmt::Display for EmbeddedCliErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EmbeddedCliErrorKind::CreateDir => f.write_str("failed to create install directory"), - #[cfg(any(feature = "bundled-in-process", not(windows)))] EmbeddedCliErrorKind::Archive => f.write_str("failed to read archive entry"), - #[cfg(all(not(feature = "bundled-in-process"), windows))] - EmbeddedCliErrorKind::Zip => f.write_str("failed to read zip archive"), EmbeddedCliErrorKind::BinaryNotFoundInArchive => { f.write_str("CLI binary not found in embedded archive") } @@ -670,7 +858,7 @@ mod tests { #[cfg(all(has_bundled_cli, feature = "bundled-in-process"))] #[test] - fn embedded_archive_contains_only_expected_files() { + fn embedded_archive_contains_runtime_assets_and_excludes_cli_only_files() { let gz = flate2::read::GzDecoder::new(build_time::CLI_ARCHIVE); let mut archive = tar::Archive::new(gz); let mut names: Vec = archive @@ -687,12 +875,13 @@ mod tests { .collect(); names.sort(); - let mut expected = vec![ - CLI_BINARY_NAME.to_string(), - RUNTIME_LIBRARY_NAME.to_string(), - ]; - expected.sort(); - assert_eq!(names, expected); + assert!(names.contains(&CLI_BINARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_LIBRARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_BINARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_NODE_NAME.to_string())); + assert!(names.iter().any(|name| name.starts_with("ripgrep/"))); + assert!(names.iter().any(|name| name.starts_with("definitions/"))); + assert!(!names.contains(&"app.js".to_string())); } /// Bytes whose header looks like a valid executable image on the host @@ -841,4 +1030,42 @@ mod tests { assert_eq!(mode & 0o777, 0o755, "temp binary should be executable"); } } + + #[cfg(has_bundled_cli)] + #[test] + fn runtime_install_replaces_stale_pair() { + let dir = tempfile::tempdir().expect("tempdir"); + fs::write(dir.path().join(RUNTIME_NODE_NAME), b"stale runtime").expect("seed runtime"); + fs::write(dir.path().join(RUNTIME_BINARY_NAME), b"stale wrapper").expect("seed wrapper"); + + install_runtime(dir.path(), build_time::CLI_ARCHIVE).expect("install runtime"); + + assert_eq!( + fs::read(dir.path().join(RUNTIME_NODE_NAME)).expect("read runtime"), + extract_binary(build_time::CLI_ARCHIVE, RUNTIME_NODE_NAME).expect("extract runtime") + ); + assert_eq!( + fs::read(dir.path().join(RUNTIME_BINARY_NAME)).expect("read wrapper"), + extract_binary(build_time::CLI_ARCHIVE, RUNTIME_BINARY_NAME).expect("extract wrapper") + ); + } + + #[cfg(has_bundled_cli)] + #[test] + fn custom_runtime_install_dir_isolated_by_version() { + let dir = tempfile::tempdir().expect("tempdir"); + + assert_eq!( + runtime_install_dir(dir.path(), "1.0.0").expect("claim directory"), + dir.path() + ); + assert_eq!( + runtime_install_dir(dir.path(), "1.0.0").expect("reuse directory"), + dir.path() + ); + assert_eq!( + runtime_install_dir(dir.path(), "2.0.0").expect("isolate directory"), + dir.path().join("2.0.0") + ); + } } diff --git a/rust/src/extension_launch_provider.rs b/rust/src/extension_launch_provider.rs new file mode 100644 index 0000000000..63e2284144 --- /dev/null +++ b/rust/src/extension_launch_provider.rs @@ -0,0 +1,158 @@ +//! Connection-level extension launch profile resolution. + +use std::sync::{Arc, OnceLock, Weak}; + +use async_trait::async_trait; +use parking_lot::RwLock; +use serde::Serialize; +use serde_json::Value; +use tracing::warn; + +pub use crate::rpc::{ + ExtensionLaunchProfile, ExtensionLaunchProviderResolveRequest, + ExtensionLaunchProviderResolveResult, +}; +use crate::{ + Client, ClientInner, JsonRpcError, JsonRpcRequest, JsonRpcResponse, Result, error_codes, +}; + +pub(crate) const RESOLVE_METHOD: &str = "extensionLaunchProvider.resolve"; +const MISSING_HANDLER_MESSAGE: &str = "No extensionLaunchProvider client-global handler registered"; + +/// Resolves process launch profiles for extension entrypoints discovered by the runtime. +/// +/// Configure an implementation with +/// [`ClientOptions::with_extension_launch_provider`](crate::ClientOptions::with_extension_launch_provider). +/// The SDK registers the provider before [`Client::start`](crate::Client::start) +/// returns, so extension resolution cannot race session creation. +/// +/// The returned executable, arguments, and environment are forwarded unchanged. +/// The runtime remains authoritative for its reserved `COPILOT_SDK_PATH`, +/// `SESSION_ID`, and `COPILOT_EXTENSION_PARENT_PID` environment variables. +#[async_trait] +pub trait ExtensionLaunchProvider: Send + Sync + 'static { + /// Resolve a launch profile for one discovered extension entrypoint. + /// + /// Return a result with `launch: None` when the provider does not support + /// the entrypoint. + async fn resolve( + &self, + request: ExtensionLaunchProviderResolveRequest, + ) -> Result; +} + +pub(crate) struct ExtensionLaunchProviderDispatcher { + handler: RwLock>>, + client: OnceLock>, +} + +impl ExtensionLaunchProviderDispatcher { + pub(crate) fn new(handler: Option>) -> Self { + Self { + handler: RwLock::new(handler), + client: OnceLock::new(), + } + } + + pub(crate) fn set_client(&self, client: Weak) { + let _ = self.client.set(client); + } + + pub(crate) fn is_configured(&self) -> bool { + self.handler.read().is_some() + } + + pub(crate) fn clear(&self) { + self.handler.write().take(); + } + + pub(crate) async fn dispatch(&self, request: JsonRpcRequest) { + let request_id = request.id; + let Some(handler) = self.handler.read().clone() else { + self.send_error( + request_id, + error_codes::INTERNAL_ERROR, + MISSING_HANDLER_MESSAGE, + ) + .await; + return; + }; + + let params = request + .params + .unwrap_or_else(|| Value::Object(serde_json::Map::new())); + let request = match serde_json::from_value(params) { + Ok(request) => request, + Err(error) => { + self.send_error( + request_id, + error_codes::INVALID_PARAMS, + &format!("invalid params: {error}"), + ) + .await; + return; + } + }; + + match handler.resolve(request).await { + Ok(result) => self.respond(request_id, result).await, + Err(error) => { + self.send_error(request_id, error_codes::INTERNAL_ERROR, &error.to_string()) + .await; + } + } + } + + fn client(&self) -> Option { + self.client + .get() + .and_then(Weak::upgrade) + .map(Client::from_inner) + } + + async fn respond(&self, request_id: u64, result: T) { + let value = match serde_json::to_value(result) { + Ok(value) => value, + Err(error) => { + warn!(error = %error, "failed to serialize extension launch provider response"); + self.send_error( + request_id, + error_codes::INTERNAL_ERROR, + "serialization failure", + ) + .await; + return; + } + }; + + let Some(client) = self.client() else { + return; + }; + let _ = client + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request_id, + result: Some(value), + error: None, + }) + .await; + } + + async fn send_error(&self, request_id: u64, code: i32, message: &str) { + let Some(client) = self.client() else { + return; + }; + let _ = client + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request_id, + result: None, + error: Some(JsonRpcError { + code, + message: message.to_string(), + data: None, + }), + }) + .await; + } +} diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs index f784b1a6d1..a25990062c 100644 --- a/rust/src/ffi.rs +++ b/rust/src/ffi.rs @@ -2,12 +2,11 @@ //! library and speaking JSON-RPC over its C ABI, //! instead of spawning a CLI child process and communicating over stdio/TCP. //! -//! The runtime's `host_start` export spawns the residual TypeScript worker -//! itself — the packaged single-file CLI (`copilot --embedded-host`) or, for -//! dev, `node dist-cli/index.js --embedded-host`. JSON-RPC frames are pumped -//! across the ABI: writes go to `connection_write`; inbound frames arrive on a -//! native callback that feeds an async reader. The framing is unchanged — the -//! same LSP `Content-Length:` frames the stdio transport uses. +//! The runtime's `host_start` export constructs the Rust server synchronously in +//! this process. JSON-RPC frames are pumped across the ABI: writes go to +//! `connection_write`; inbound frames arrive on a native callback that feeds an +//! async reader. The framing is unchanged — the same LSP `Content-Length:` +//! frames the stdio transport uses. use std::collections::HashMap; use std::ffi::c_void; @@ -204,12 +203,11 @@ impl AsyncWrite for FfiWriter { } } -/// Prepared FFI host: the bound cdylib exports plus the spawn arguments needed -/// to start the runtime worker. The cdylib is loaded process-globally and never -/// unloaded (see [`load_library`]). +/// Prepared FFI host. The cdylib is loaded process-globally and never unloaded +/// (see [`load_library`]). pub(crate) struct FfiHost { library_path: PathBuf, - entrypoint: PathBuf, + cli_entrypoint: Option, environment: Vec<(String, String)>, args: Vec, host_start: HostStartFn, @@ -224,30 +222,34 @@ pub(crate) struct FfiHost { unsafe impl Send for FfiHost {} impl FfiHost { - /// Load the cdylib next to `entrypoint` and bind its exports. - /// - /// `entrypoint` is the packaged single-file CLI binary or, for dev, a - /// `.js` file launched via `node`. The native library is resolved relative - /// to the entrypoint directory, supporting both packaged and development - /// layouts. + /// Load the cdylib next to `runtime_entrypoint` and bind its exports. pub(crate) fn create( - entrypoint: &Path, + runtime_entrypoint: &Path, + cli_entrypoint: Option<&Path>, environment: Vec<(String, String)>, args: Vec, ) -> Result { - let entrypoint = std::fs::canonicalize(entrypoint) - .map(path_for_child_process) + let runtime_entrypoint = std::fs::canonicalize(runtime_entrypoint).map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!( + "failed to resolve in-process runtime entrypoint '{}': {e}", + runtime_entrypoint.display() + ), + ) + })?; + let cli_entrypoint = cli_entrypoint + .map(std::fs::canonicalize) + .transpose() .map_err(|e| { Error::with_message( ErrorKind::InvalidConfig, - format!( - "failed to resolve in-process CLI entrypoint '{}': {e}", - entrypoint.display() - ), + format!("failed to resolve explicit in-process CLI entrypoint: {e}"), ) - })?; - let library_path = - std::fs::canonicalize(resolve_library_path(&entrypoint)?).map_err(|e| { + })? + .map(path_for_child_process); + let library_path = std::fs::canonicalize(resolve_library_path(&runtime_entrypoint)?) + .map_err(|e| { Error::with_message( ErrorKind::InvalidConfig, format!("failed to resolve in-process runtime library: {e}"), @@ -267,7 +269,7 @@ impl FfiHost { Ok(Self { library_path, - entrypoint, + cli_entrypoint, environment, args, host_start, @@ -278,11 +280,7 @@ impl FfiHost { }) } - /// Start the runtime worker and open the FFI JSON-RPC connection. - /// - /// `host_start` blocks until the worker connects back and signals - /// readiness (up to ~30s), and must not run on an async executor thread, so - /// the blocking handshake is offloaded to [`tokio::task::spawn_blocking`]. + /// Start the native runtime and open the FFI JSON-RPC connection. pub(crate) async fn start(self) -> Result<(FfiReader, FfiWriter, Arc), Error> { tokio::task::spawn_blocking(move || self.start_blocking()) .await @@ -295,7 +293,7 @@ impl FfiHost { } fn start_blocking(self) -> Result<(FfiReader, FfiWriter, Arc), Error> { - let argv = build_argv_json(&self.entrypoint, &self.args); + let argv = build_argv_json(self.cli_entrypoint.as_deref(), &self.args); let env = build_env_json(&self.environment); let (env_ptr, env_len) = match &env { @@ -309,9 +307,8 @@ impl FfiHost { return Err(Error::with_message( ErrorKind::InvalidConfig, format!( - "copilot_runtime_host_start failed (library '{}', entrypoint '{}')", - self.library_path.display(), - self.entrypoint.display() + "copilot_runtime_host_start failed (library '{}')", + self.library_path.display() ), )); } @@ -440,6 +437,8 @@ pub(crate) fn prebuilds_folder() -> Option { "win32" } else if cfg!(target_os = "macos") { "darwin" + } else if cfg!(all(target_os = "linux", target_env = "musl")) { + "linuxmusl" } else if cfg!(target_os = "linux") { "linux" } else { @@ -472,6 +471,11 @@ fn resolve_library_path(entrypoint: &Path) -> Result { return Ok(flat); } + let adjacent = dir.join("runtime.node"); + if adjacent.is_file() { + return Ok(adjacent); + } + // Development package layout. let prebuilds = prebuilds_folder().map(|folder| dir.join("prebuilds").join(folder).join("runtime.node")); @@ -521,28 +525,23 @@ fn path_for_child_process(path: PathBuf) -> PathBuf { path } -fn build_argv_json(entrypoint: &Path, extra_args: &[String]) -> Vec { - // A `.js` entrypoint (dev / dist-cli) is launched via node; the packaged - // single-file CLI binary embeds its own Node and is invoked directly. - let entrypoint_str = entrypoint.to_string_lossy().into_owned(); - let is_js = entrypoint - .extension() - .and_then(|ext| ext.to_str()) - .is_some_and(|ext| ext.eq_ignore_ascii_case("js")); - let mut argv: Vec = if is_js { - vec![ - "node".to_string(), - entrypoint_str, - "--embedded-host".to_string(), - "--no-auto-update".to_string(), - ] - } else { - vec![ +fn build_argv_json(entrypoint: Option<&Path>, extra_args: &[String]) -> Vec { + let mut argv = Vec::new(); + if let Some(entrypoint) = entrypoint { + let entrypoint_str = entrypoint.to_string_lossy().into_owned(); + if entrypoint + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("js")) + { + argv.push("node".to_string()); + } + argv.extend([ entrypoint_str, "--embedded-host".to_string(), "--no-auto-update".to_string(), - ] - }; + ]); + } argv.extend_from_slice(extra_args); serde_json::to_vec(&argv).expect("argv serializes") } @@ -563,29 +562,20 @@ mod tests { use super::*; #[test] - fn argv_pins_worker_and_appends_client_options() { + fn argv_without_entrypoint_contains_only_client_options() { let argv: Vec = serde_json::from_slice(&build_argv_json( - Path::new("copilot"), + None, &["--log-level".into(), "debug".into()], )) .unwrap(); - assert_eq!( - argv, - [ - "copilot", - "--embedded-host", - "--no-auto-update", - "--log-level", - "debug" - ] - ); + assert_eq!(argv, ["--log-level", "debug"]); } #[test] - fn javascript_entrypoint_uses_node() { + fn explicit_javascript_entrypoint_uses_node() { let argv: Vec = - serde_json::from_slice(&build_argv_json(Path::new("index.js"), &[])).unwrap(); + serde_json::from_slice(&build_argv_json(Some(Path::new("index.js")), &[])).unwrap(); assert_eq!( argv, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 4a9f73ca4c..1c20fd1837 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -10,6 +10,8 @@ mod canvas_dispatch; #[cfg(feature = "bundled-cli")] pub(crate) mod embeddedcli; mod errors; +/// Connection-level extension launch profile provider. +pub mod extension_launch_provider; /// In-process FFI transport hosting the runtime cdylib (`Transport::InProcess`). #[cfg(feature = "bundled-in-process")] pub(crate) mod ffi; @@ -179,8 +181,10 @@ pub enum Transport { /// How the SDK locates the GitHub Copilot CLI binary. #[derive(Debug, Clone, Default)] pub enum CliProgram { - /// Auto-resolve: `COPILOT_CLI_PATH` → embedded CLI → dev cache. - /// This is the default. + /// Auto-resolve the transport's program. Managed child-process transports + /// select `COPILOT_CLI_PATH`, then the bundled runtime wrapper. In-process + /// transport loads the wrapper's adjacent runtime library directly unless + /// `COPILOT_CLI_PATH` explicitly selects a legacy embedded host. #[default] Resolve, /// Use an explicit binary path (skips resolution). @@ -204,12 +208,9 @@ pub const HAS_BUNDLED_CLI: bool = cfg!(has_bundled_cli); /// Returns the path to the bundled Copilot CLI, extracting it from the /// embedded archive on first call. /// -/// This is the same path [`Client::start`] resolves to when -/// [`ClientOptions::program`] is [`CliProgram::Resolve`], no -/// `COPILOT_CLI_PATH` override is set, and no -/// [`ClientOptions::bundled_cli_extract_dir`] is configured — exposing -/// it directly so callers (health checks, diagnostics, version probes) -/// can reach the bundled binary without spinning up a full [`Client`]. +/// This exposes the CLI artifact directly for callers such as health checks, +/// diagnostics, version probes, and in-process hosting. Managed child-process +/// transports resolve the bundled `copilot-runtime` wrapper instead. /// /// Subsequent calls return the cached result. Extraction is skipped when /// an already-published binary passes a cheap integrity re-check; a @@ -235,12 +236,35 @@ pub fn install_bundled_cli() -> Option { } } +/// Returns the path to the bundled `copilot-runtime` executable, extracting it +/// with adjacent `runtime.node` on first call. +/// +/// This is intended for health checks and intermediate launchers that need the +/// concrete managed runtime path before [`Client::start`]. Subsequent calls +/// return the cached result. +/// +/// Returns `None` when the `bundled-cli` feature is off, the target platform +/// isn't supported, or extraction failed. It does not fall back to the +/// build-time extraction cache. +pub fn install_bundled_runtime() -> Option { + #[cfg(feature = "bundled-cli")] + { + embeddedcli::runtime_path() + } + #[cfg(not(feature = "bundled-cli"))] + { + None + } +} + /// Options for starting a [`Client`]. /// /// When `program` is [`CliProgram::Resolve`] (the default), [`Client::start`] -/// uses `COPILOT_CLI_PATH` when set to a real file. Otherwise it uses the -/// bundled Copilot CLI when the default `bundled-cli` cargo feature is enabled, -/// or the build-time extracted dev-cache CLI when that feature is disabled. +/// uses `COPILOT_CLI_PATH` when set to a real file. Managed child-process +/// transports next use the bundled `copilot-runtime` wrapper. In-process +/// transport loads the wrapper's adjacent runtime library. With `bundled-cli` +/// disabled, the corresponding artifact is resolved from the build-time +/// extraction cache. /// /// Set `program` to [`CliProgram::Path`] to use an explicit binary instead. /// This skips auto-resolution entirely. @@ -311,6 +335,14 @@ pub struct ClientOptions { /// [`CopilotRequestHandler`] /// instead of issuing the calls itself. pub request_handler: Option>, + /// Connection-level extension launch profile provider. + /// + /// When set, the SDK registers itself with the runtime during + /// [`Client::start`] before any session can be created. Incoming + /// `extensionLaunchProvider.resolve` requests are dispatched independently + /// of sessions. + pub extension_launch_provider: + Option>, /// Connection-level GitHub telemetry forwarding callback (experimental). /// /// When set, every session created or resumed on this client opts into @@ -403,6 +435,10 @@ impl std::fmt::Debug for ClientOptions { "request_handler", &self.request_handler.as_ref().map(|_| ""), ) + .field( + "extension_launch_provider", + &self.extension_launch_provider.as_ref().map(|_| ""), + ) .field( "on_github_telemetry", &self.on_github_telemetry.as_ref().map(|_| ""), @@ -656,6 +692,7 @@ impl Default for ClientOptions { on_list_models: None, session_fs: None, request_handler: None, + extension_launch_provider: None, on_github_telemetry: None, on_get_trace_context: None, telemetry: None, @@ -814,6 +851,18 @@ impl ClientOptions { self } + /// Register a connection-level extension launch profile provider. + /// + /// The provider is wrapped in [`Arc`] internally and registered with the + /// runtime before [`Client::start`] returns. + pub fn with_extension_launch_provider

(mut self, provider: P) -> Self + where + P: crate::extension_launch_provider::ExtensionLaunchProvider, + { + self.extension_launch_provider = Some(Arc::new(provider)); + self + } + /// Register a connection-level GitHub telemetry forwarding callback /// (internal/experimental). Registering a callback auto-enables telemetry /// forwarding on every session created or resumed on this client; the @@ -859,8 +908,8 @@ impl ClientOptions { self } - /// Override the directory where the bundled CLI binary is extracted on - /// first use. See [`Self::bundled_cli_extract_dir`]. + /// Override the directory where bundled CLI and runtime artifacts are + /// extracted on first use. See [`Self::bundled_cli_extract_dir`]. /// /// Only applies when the `bundled-cli` cargo feature is on. With /// `bundled-cli` disabled (`default-features = false`), set @@ -1037,6 +1086,7 @@ struct ClientInner { /// Inbound `llmInference.*` dispatcher, installed when /// [`ClientOptions::request_handler`] is set. llm_inference: OnceLock>, + extension_launch_provider: Arc, /// Connection-level GitHub telemetry forwarding callback, set from /// [`ClientOptions::on_github_telemetry`]. Drives the /// `enableGitHubTelemetryForwarding` wire flag and the @@ -1182,6 +1232,7 @@ impl Client { }; let session_fs_config = options.session_fs.clone(); let request_handler = options.request_handler.clone(); + let extension_launch_provider = options.extension_launch_provider.clone(); let session_fs_sqlite_declared = session_fs_config .as_ref() .and_then(|c| c.capabilities.as_ref()) @@ -1195,6 +1246,7 @@ impl Client { let resolve_start = Instant::now(); let resolved = resolve::copilot_binary_with_extract_dir( options.bundled_cli_extract_dir.as_deref(), + true, )?; let resolve_elapsed = resolve_start.elapsed(); timings.program_resolve_ms = Some(StartupTimings::millis(resolve_elapsed)); @@ -1202,7 +1254,7 @@ impl Client { elapsed_ms = resolve_elapsed.as_millis(), "Client::start CLI program resolution complete" ); - info!(path = %resolved.display(), "resolved copilot CLI"); + info!(path = %resolved.display(), "resolved copilot runtime"); #[cfg(windows)] { if let Some(ext) = resolved.extension().and_then(|e| e.to_str()).filter(|ext| { @@ -1252,6 +1304,7 @@ impl Client { None, working_directory, options.on_list_models, + extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, options.on_get_trace_context, @@ -1283,6 +1336,7 @@ impl Client { Some(child), working_directory, options.on_list_models, + extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, options.on_get_trace_context, @@ -1304,6 +1358,7 @@ impl Client { Some(child), working_directory, options.on_list_models, + extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, options.on_get_trace_context, @@ -1353,7 +1408,15 @@ impl Client { if !use_logged_in_user { args.push("--no-auto-login".to_string()); } - let host = crate::ffi::FfiHost::create(&program, environment, args)?; + let explicit_cli = std::env::var_os("COPILOT_CLI_PATH") + .map(PathBuf::from) + .filter(|path| path.is_file()); + let host = crate::ffi::FfiHost::create( + &program, + explicit_cli.as_deref(), + environment, + args, + )?; let (reader, writer, shared) = host.start().await?; let client = Self::from_transport( reader, @@ -1361,6 +1424,7 @@ impl Client { None, working_directory, options.on_list_models, + extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, options.on_get_trace_context, @@ -1387,6 +1451,25 @@ impl Client { elapsed_ms = start_time.elapsed().as_millis(), "Client::start protocol verification complete" ); + let request_dispatcher = request_handler.map(|handler| { + let dispatcher = Arc::new(copilot_request_handler::CopilotRequestDispatcher::new( + handler, + )); + dispatcher.set_client(Arc::downgrade(&client.inner)); + let _ = client.inner.llm_inference.set(dispatcher.clone()); + dispatcher + }); + if client.inner.extension_launch_provider.is_configured() { + client.inner.router.ensure_started( + &client.inner.notification_tx, + &client.inner.request_rx, + client.inner.extension_launch_provider.clone(), + request_dispatcher.clone(), + client.inner.on_github_telemetry.clone(), + client.inner.github_token_registry.clone(), + ); + client.rpc().register_extension_launch_provider().await?; + } if !builtin_plugin_directories.is_empty() { client .call( @@ -1416,18 +1499,14 @@ impl Client { "Client::start session filesystem setup complete" ); } - if let Some(handler) = request_handler { + if let Some(dispatcher) = request_dispatcher { let llm_inference_start = Instant::now(); - let dispatcher = Arc::new(copilot_request_handler::CopilotRequestDispatcher::new( - handler, - )); - dispatcher.set_client(Arc::downgrade(&client.inner)); - let _ = client.inner.llm_inference.set(dispatcher.clone()); // Start the router early (before any session is registered) so the // startup model catalog request is dispatched to the handler. client.inner.router.ensure_started( &client.inner.notification_tx, &client.inner.request_rx, + client.inner.extension_launch_provider.clone(), Some(dispatcher.clone()), client.inner.on_github_telemetry.clone(), client.inner.github_token_registry.clone(), @@ -1486,6 +1565,33 @@ impl Client { None, cwd, None, + None, + false, + false, + None, + None, + None, + ClientMode::default(), + ) + } + + /// Construct a [`Client`] from raw streams with a preset extension launch + /// provider, for integration testing connection-global reverse requests. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_extension_launch_provider( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + provider: Arc, + ) -> Result { + Self::from_transport( + reader, + writer, + None, + cwd, + None, + Some(provider), false, false, None, @@ -1515,6 +1621,7 @@ impl Client { None, cwd, None, + None, false, false, Some(provider), @@ -1540,6 +1647,7 @@ impl Client { None, cwd, None, + None, false, false, None, @@ -1565,6 +1673,7 @@ impl Client { None, cwd, None, + None, false, false, None, @@ -1591,6 +1700,9 @@ impl Client { child: Option, cwd: PathBuf, on_list_models: Option>, + extension_launch_provider: Option< + Arc, + >, session_fs_configured: bool, session_fs_sqlite_declared: bool, on_get_trace_context: Option>, @@ -1612,6 +1724,11 @@ impl Client { info!(pid = ?pid, "copilot CLI client ready"); let github_token_registry = Arc::new(github_token::GitHubTokenRegistry::new()); + let extension_launch_provider = Arc::new( + extension_launch_provider::ExtensionLaunchProviderDispatcher::new( + extension_launch_provider, + ), + ); let client = Self { inner: Arc::new(ClientInner { child: parking_lot::Mutex::new(child), @@ -1631,6 +1748,7 @@ impl Client { session_fs_configured, session_fs_sqlite_declared, llm_inference: OnceLock::new(), + extension_launch_provider: extension_launch_provider.clone(), on_github_telemetry, on_get_trace_context, effective_connection_token, @@ -1639,6 +1757,7 @@ impl Client { }), }; github_token_registry.set_client(Arc::downgrade(&client.inner)); + extension_launch_provider.set_client(Arc::downgrade(&client.inner)); client.spawn_lifecycle_dispatcher(); debug!( elapsed_ms = setup_start.elapsed().as_millis(), @@ -2055,6 +2174,7 @@ impl Client { self.inner.router.ensure_started( &self.inner.notification_tx, &self.inner.request_rx, + self.inner.extension_launch_provider.clone(), self.inner.llm_inference.get().cloned(), self.inner.on_github_telemetry.clone(), self.inner.github_token_registry.clone(), @@ -2074,6 +2194,7 @@ impl Client { self.inner.router.ensure_started( &self.inner.notification_tx, &self.inner.request_rx, + self.inner.extension_launch_provider.clone(), self.inner.llm_inference.get().cloned(), self.inner.on_github_telemetry.clone(), self.inner.github_token_registry.clone(), @@ -2293,6 +2414,7 @@ impl Client { self.inner.router.ensure_started( &self.inner.notification_tx, &self.inner.request_rx, + self.inner.extension_launch_provider.clone(), self.inner.llm_inference.get().cloned(), self.inner.on_github_telemetry.clone(), self.inner.github_token_registry.clone(), @@ -2466,6 +2588,7 @@ impl Client { let pid = self.pid(); info!(pid = ?pid, "stopping CLI process"); let mut errors: Vec = Vec::new(); + self.inner.extension_launch_provider.clear(); // Snapshot the registered session IDs without holding the router // lock across the destroy RPCs. @@ -2551,12 +2674,12 @@ impl Client { } } - // The runtime.shutdown RPC above already asked the runtime to clean up; - // closing here tears down the transport. + // Provider registration is scoped to the connection. Closing the + // transport unregisters it and prevents stale callbacks after stop. + self.inner.rpc.force_close(); #[cfg(feature = "bundled-in-process")] { if let Some(host) = self.inner.ffi_host.lock().take() { - self.inner.rpc.force_close(); host.close(); } } @@ -2601,6 +2724,7 @@ impl Client { pub fn force_stop(&self) { let pid = self.pid(); info!(pid = ?pid, "force-stopping CLI process"); + self.inner.extension_launch_provider.clear(); if let Some(mut child) = self.inner.child.lock().take() && let Err(e) = child.start_kill() { @@ -3325,6 +3449,7 @@ mod tests { Some(child), temp.path().to_path_buf(), None, + None, false, false, None, @@ -3433,6 +3558,9 @@ mod tests { session_fs_configured: false, session_fs_sqlite_declared: false, llm_inference: OnceLock::new(), + extension_launch_provider: Arc::new( + extension_launch_provider::ExtensionLaunchProviderDispatcher::new(None), + ), on_github_telemetry: None, on_get_trace_context: None, effective_connection_token: None, diff --git a/rust/src/resolve.rs b/rust/src/resolve.rs index 1c88283a27..d8b996a11a 100644 --- a/rust/src/resolve.rs +++ b/rust/src/resolve.rs @@ -5,9 +5,9 @@ //! 1. An explicit path supplied by the application via //! [`CliProgram::Path`](crate::CliProgram::Path). //! 2. The `COPILOT_CLI_PATH` environment variable. -//! 3. The bundled CLI embedded in this crate at build time (when the +//! 3. The bundled program embedded in this crate at build time (when the //! `bundled-cli` cargo feature is on, the default). -//! 4. The build-time-extracted CLI in the per-user cache (when +//! 4. The build-time-extracted program in the per-user cache (when //! `bundled-cli` is off). //! //! There is no PATH scanning and no walking of standard install locations. @@ -35,6 +35,7 @@ use crate::{Error, ErrorKind}; /// under it. pub(crate) fn copilot_binary_with_extract_dir( extract_dir: Option<&Path>, + use_runtime_wrapper: bool, ) -> Result { if let Ok(value) = env::var("COPILOT_CLI_PATH") { let candidate = PathBuf::from(&value); @@ -49,11 +50,21 @@ pub(crate) fn copilot_binary_with_extract_dir( #[cfg(feature = "bundled-cli")] { - let bundled = match extract_dir { - Some(dir) => crate::embeddedcli::install_at(dir), - None => crate::embeddedcli::path(), + let bundled = if use_runtime_wrapper { + match extract_dir { + Some(dir) => crate::embeddedcli::install_runtime_at(dir), + None => crate::embeddedcli::runtime_path(), + } + } else { + match extract_dir { + Some(dir) => crate::embeddedcli::install_at(dir), + None => crate::embeddedcli::path(), + } }; if let Some(path) = bundled { + if use_runtime_wrapper { + validate_runtime_pair(&path)?; + } return Ok(path); } } @@ -61,16 +72,21 @@ pub(crate) fn copilot_binary_with_extract_dir( #[cfg(not(feature = "bundled-cli"))] { let _ = extract_dir; - if let Some(path) = extracted_cli_path() { - return Ok(path); + if let Some(program) = extracted_program(use_runtime_wrapper) { + return Ok(program); } } + let binary_name = if use_runtime_wrapper { + runtime_binary_name() + } else { + cli_binary_name() + }; Err(ErrorKind::BinaryNotFound { - name: "copilot".into(), + name: binary_name.into(), hint: Some( "the Copilot CLI is not bundled in this build of github-copilot-sdk and \ - COPILOT_CLI_PATH is not set. Either keep the default `bundled-cli` cargo \ + no applicable path override is set. Either keep the default `bundled-cli` cargo \ feature enabled, set COPILOT_CLI_PATH, or supply an explicit path via \ `CliProgram::Path(...)` on `ClientOptions::program`." .into(), @@ -79,7 +95,7 @@ pub(crate) fn copilot_binary_with_extract_dir( .into()) } -/// Path to the CLI extracted into the per-user cache by `build.rs` when +/// Path to the program extracted into the per-user cache by `build.rs` when /// `bundled-cli` is disabled. Returns `None` if the cached file is missing /// (e.g. the user deleted the cache after building, or built with /// `COPILOT_SKIP_CLI_DOWNLOAD`). @@ -93,14 +109,8 @@ pub(crate) fn copilot_binary_with_extract_dir( /// `$HOME` / `$LOCALAPPDATA` into the artifact, breaks sccache across /// machines, and prevents copying `target/` between hosts. #[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))] -fn extracted_cli_path() -> Option { +fn extracted_program(use_runtime_wrapper: bool) -> Option { let version = env!("COPILOT_SDK_CLI_VERSION"); - let binary = if cfg!(windows) { - "copilot.exe" - } else { - "copilot" - }; - let dir = match env::var_os("COPILOT_CLI_EXTRACT_DIR") { Some(custom) => PathBuf::from(custom), None => dirs::cache_dir() @@ -110,8 +120,16 @@ fn extracted_cli_path() -> Option { .join(sanitize_version(version)), }; - let path = dir.join(binary); - if path.is_file() { + let path = dir.join(if use_runtime_wrapper { + runtime_binary_name() + } else { + cli_binary_name() + }); + if use_runtime_wrapper { + if validate_runtime_pair(&path).is_ok() { + return Some(path); + } + } else if path.is_file() { return Some(path); } warn!( @@ -125,10 +143,83 @@ fn extracted_cli_path() -> Option { /// build opted out via `COPILOT_SKIP_CLI_DOWNLOAD`. In both cases there's /// no binary to look up, so the resolver returns `None` immediately. #[cfg(all(not(feature = "bundled-cli"), not(has_extracted_cli)))] -fn extracted_cli_path() -> Option { +fn extracted_program(_use_runtime_wrapper: bool) -> Option { None } +fn validate_runtime_pair(wrapper: &Path) -> Result<(), Error> { + let wrapper_valid = wrapper + .metadata() + .map(|metadata| metadata.is_file() && metadata.len() > 0) + .unwrap_or(false); + let runtime_node = wrapper + .parent() + .map(|parent| parent.join("runtime.node")) + .unwrap_or_else(|| PathBuf::from("runtime.node")); + let runtime_valid = runtime_node + .metadata() + .map(|metadata| metadata.is_file() && metadata.len() > 0) + .unwrap_or(false); + if wrapper_valid && runtime_valid { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let metadata = wrapper.metadata().map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!( + "failed to inspect Copilot runtime wrapper permissions at '{}': {e}", + wrapper.display() + ), + ) + })?; + if metadata.permissions().mode() & 0o111 == 0 { + let mut permissions = metadata.permissions(); + permissions.set_mode(permissions.mode() | 0o111); + std::fs::set_permissions(wrapper, permissions).map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!( + "failed to make Copilot runtime wrapper executable at '{}': {e}", + wrapper.display() + ), + ) + })?; + } + } + return Ok(()); + } + let detail = format!( + "The runtime wrapper and its adjacent runtime.node must both be non-empty files; checked '{}' and '{}'", + wrapper.display(), + runtime_node.display() + ); + Err(Error::with_message( + ErrorKind::BinaryNotFound { + name: runtime_binary_name().into(), + hint: Some(detail.clone()), + }, + detail, + )) +} + +fn cli_binary_name() -> &'static str { + if cfg!(windows) { + "copilot.exe" + } else { + "copilot" + } +} + +fn runtime_binary_name() -> &'static str { + if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + } +} + /// Replace characters outside `[a-zA-Z0-9._-]` with `_`. Kept in sync /// with `build.rs::sanitize_version` and `embeddedcli::sanitize_version` /// so all three resolve to the same cache directory for any given @@ -143,3 +234,29 @@ fn sanitize_version(version: &str) -> String { }) .collect() } + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::validate_runtime_pair; + + #[test] + fn runtime_pair_requires_adjacent_nonempty_runtime_node() { + let dir = tempdir().expect("temp dir"); + let wrapper = dir.path().join(if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + }); + fs::write(&wrapper, b"wrapper").expect("write wrapper"); + + let error = validate_runtime_pair(&wrapper).expect_err("runtime.node is required"); + assert!(error.to_string().contains("runtime.node")); + + fs::write(dir.path().join("runtime.node"), b"runtime").expect("write runtime.node"); + validate_runtime_pair(&wrapper).expect("complete pair is valid"); + } +} diff --git a/rust/src/router.rs b/rust/src/router.rs index 1dec9d16f3..4815d0e1c5 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -85,6 +85,9 @@ impl SessionRouter { &self, notification_tx: &broadcast::Sender, request_rx: &Mutex>>, + extension_launch_provider: Arc< + crate::extension_launch_provider::ExtensionLaunchProviderDispatcher, + >, llm_inference: Option>, github_telemetry: Option, github_token_registry: Arc, @@ -182,6 +185,10 @@ impl SessionRouter { let sessions = self.sessions.clone(); tokio::spawn(async move { while let Some(request) = rx.recv().await { + if request.method == crate::extension_launch_provider::RESOLVE_METHOD { + extension_launch_provider.dispatch(request).await; + continue; + } if request.method == "gitHubToken.getToken" { github_token_registry.dispatch(request).await; continue; diff --git a/rust/src/startup_timings.rs b/rust/src/startup_timings.rs index 7938a462b7..7784a6ab3b 100644 --- a/rust/src/startup_timings.rs +++ b/rust/src/startup_timings.rs @@ -38,7 +38,7 @@ use std::time::Duration; #[non_exhaustive] pub struct StartupTimings { /// Time spent in `resolve::copilot_binary_with_extract_dir` locating (and, - /// for a bundled CLI, extracting) the copilot binary. `None` when the + /// for bundled artifacts, extracting) the Copilot program. `None` when the /// caller passes an explicit [`CliProgram::Path`](crate::CliProgram::Path). pub program_resolve_ms: Option, /// Time spent spawning the CLI subprocess (`command.spawn()`). `None` for diff --git a/rust/tests/cli_resolution_test.rs b/rust/tests/cli_resolution_test.rs index 9e4927e676..75773a0c0d 100644 --- a/rust/tests/cli_resolution_test.rs +++ b/rust/tests/cli_resolution_test.rs @@ -10,7 +10,10 @@ use std::path::PathBuf; use github_copilot_sdk::{ CliProgram, Client, ClientOptions, ErrorKind, HAS_BUNDLED_CLI, install_bundled_cli, + install_bundled_runtime, }; +#[cfg(all(feature = "bundled-cli", has_bundled_cli))] +use github_copilot_sdk::{SessionConfig, Transport}; use serial_test::serial; fn unset_env(key: &str) { @@ -95,7 +98,7 @@ async fn stale_env_override_falls_through() { } } -/// With `bundled-cli` off, `build.rs` extracts the binary into the +/// With `bundled-cli` off, `build.rs` extracts the runtime wrapper into the /// per-user cache and the runtime resolver recomputes its location from /// `COPILOT_SDK_CLI_VERSION` + the OS-derived binary name. This test /// mirrors that convention and asserts the file is on disk where the @@ -105,9 +108,9 @@ async fn stale_env_override_falls_through() { fn extracted_binary_present_at_conventional_path() { let version = env!("COPILOT_SDK_CLI_VERSION"); let binary = if cfg!(windows) { - "copilot.exe" + "copilot-runtime.exe" } else { - "copilot" + "copilot-runtime" }; let sanitized = sanitize_version_for_test(version); let path = dirs::cache_dir() @@ -158,21 +161,19 @@ async fn unbundled_resolver_finds_extracted_binary() { /// With `bundled-cli` off, `COPILOT_CLI_EXTRACT_DIR` set at runtime /// redirects the resolver to look directly under the named directory /// (no per-version subdir, matching the build-time write semantics). -/// We place a fake `copilot[.exe]` there and assert the resolver picks -/// it up — failing here means the build-time / runtime convention has -/// drifted. #[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))] #[tokio::test(flavor = "current_thread")] #[serial(copilot_cli_path)] async fn extract_dir_runtime_override_is_honored() { let tmp = tempfile::tempdir().expect("create tempdir"); let binary = if cfg!(windows) { - "copilot.exe" + "copilot-runtime.exe" } else { - "copilot" + "copilot-runtime" }; let fake = tmp.path().join(binary); - std::fs::write(&fake, b"").expect("write fake binary"); + std::fs::write(&fake, b"runtime").expect("write fake binary"); + std::fs::write(tmp.path().join("runtime.node"), b"runtime").expect("write runtime.node"); unset_env("COPILOT_CLI_PATH"); set_env( @@ -265,6 +266,14 @@ fn install_bundled_cli_returns_extracted_path() { "install_bundled_cli returned a path that is not a file: {}", first.display() ); + assert_eq!( + first.file_name().and_then(|name| name.to_str()), + Some(if cfg!(windows) { + "copilot.exe" + } else { + "copilot" + }) + ); let second = install_bundled_cli().expect("second call should also succeed"); assert_eq!( @@ -293,30 +302,6 @@ fn install_bundled_cli_returns_extracted_path() { } } -/// `install_bundled_cli` returns the same path the runtime resolver -/// hands to `Client::start` for `CliProgram::Resolve` with no -/// `COPILOT_CLI_PATH` override. Observed indirectly: the binary the -/// public API points at must exist, and `Client::start` must not -/// report `BinaryNotFound` under the same env conditions. -#[cfg(all(feature = "bundled-cli", has_bundled_cli))] -#[tokio::test(flavor = "current_thread")] -#[serial(copilot_cli_path)] -async fn install_bundled_cli_matches_resolver() { - unset_env("COPILOT_CLI_PATH"); - unset_env("COPILOT_CLI_EXTRACT_DIR"); - - let direct = install_bundled_cli().expect("bundled CLI should install"); - assert!(direct.is_file()); - - let opts = ClientOptions::default().with_program(CliProgram::Resolve); - if let Err(e) = Client::start(opts).await { - assert!( - !matches!(e.kind(), ErrorKind::BinaryNotFound { .. }), - "resolver returned BinaryNotFound while install_bundled_cli succeeded: {e}" - ); - } -} - /// With `bundled-cli` off (or the target unsupported), the public API /// reports no bundled CLI and does not fall back to the /// build-time-extracted dev-cache path that `CliProgram::Resolve` uses. @@ -329,3 +314,97 @@ fn install_bundled_cli_is_none_without_embed() { "install_bundled_cli must not fall back to the dev-cache path" ); } + +#[cfg(all(feature = "bundled-cli", has_bundled_cli))] +#[test] +fn install_bundled_runtime_returns_wrapper_bundle() { + let first = install_bundled_runtime().expect("bundled runtime should install"); + assert_eq!( + first.file_name().and_then(|name| name.to_str()), + Some(if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + }) + ); + let runtime_node = first + .parent() + .expect("install directory") + .join("runtime.node"); + assert!( + runtime_node.is_file(), + "runtime.node was not installed: {}", + runtime_node.display() + ); + let second = install_bundled_runtime().expect("second call should also succeed"); + assert_eq!(first, second); +} + +#[cfg(all(feature = "bundled-cli", has_bundled_cli))] +#[tokio::test(flavor = "current_thread")] +#[serial(copilot_cli_path)] +async fn bundled_runtime_clean_extract_starts_without_cli_host() { + let temp = tempfile::tempdir().expect("create tempdir"); + let extract_dir = temp.path().join("runtime"); + let empty_path = temp.path().join("empty-path"); + let working_dir = temp.path().join("work"); + std::fs::create_dir(&empty_path).expect("create empty PATH directory"); + std::fs::create_dir(&working_dir).expect("create working directory"); + assert!(!extract_dir.exists()); + + let options = ClientOptions::new() + .with_bundled_cli_extract_dir(&extract_dir) + .with_cwd(&working_dir) + .with_env([("PATH", empty_path.as_os_str())]) + .with_env_remove([ + "COPILOT_RUNTIME_HOST_COMMAND", + "COPILOT_CLI_PATH", + "COPILOT_RUNTIME_PROVIDER_LIB", + ]) + .with_transport(Transport::Stdio) + .with_use_logged_in_user(false); + let client = Client::start(options) + .await + .expect("start bundled runtime from clean extraction"); + let response = client + .ping(Some("hostless runtime")) + .await + .expect("ping bundled runtime"); + assert_eq!(response.message, "pong: hostless runtime"); + + let session = client + .create_session(SessionConfig::default()) + .await + .expect("create session"); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop bundled runtime"); + + assert!(extract_dir.join("runtime.node").is_file()); + assert!( + extract_dir + .join(if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + }) + .is_file() + ); + assert!( + !extract_dir + .join(if cfg!(windows) { + "copilot.exe" + } else { + "copilot" + }) + .exists() + ); +} + +#[cfg(not(all(feature = "bundled-cli", has_bundled_cli)))] +#[test] +fn install_bundled_runtime_is_none_without_embed() { + assert!( + install_bundled_runtime().is_none(), + "install_bundled_runtime must not fall back to the dev-cache path" + ); +} diff --git a/rust/tests/e2e/rewind.rs b/rust/tests/e2e/rewind.rs index c998f96df6..44d6a39404 100644 --- a/rust/tests/e2e/rewind.rs +++ b/rust/tests/e2e/rewind.rs @@ -9,15 +9,12 @@ use github_copilot_sdk::rpc::{ use super::support::assistant_message_content; const FILE_NAME: &str = "rewind-sdk.txt"; +const ORIGINAL_FILE_CONTENT: &str = "Original rewind content"; +const PREPARED_FILE_CONTENT: &str = "Prepared rewind content"; const FILE_CONTENT: &str = "SDK rewind content"; #[tokio::test] async fn should_restore_tracked_file_and_conversation() { - // TODO(cli-1.0.81): Re-enable when Windows file-change tracking records built-in create tool writes. - if cfg!(windows) { - return; - } - super::support::with_shared_e2e_context( &E2E, "rewind", @@ -26,6 +23,7 @@ async fn should_restore_tracked_file_and_conversation() { Box::pin(async move { ctx.set_default_copilot_user(); let file_path = ctx.work_dir().join(FILE_NAME); + std::fs::write(&file_path, ORIGINAL_FILE_CONTENT).expect("write original file"); let client = ctx.start_client().await; let session = client .create_session( @@ -36,11 +34,26 @@ async fn should_restore_tracked_file_and_conversation() { .await .expect("create session"); + let ready = session + .send_and_wait(format!( + "Use the edit tool to replace the exact contents of {FILE_NAME} from \ + {ORIGINAL_FILE_CONTENT} to {PREPARED_FILE_CONTENT}. After the tool \ + succeeds, reply with exactly SDK_REWIND_READY." + )) + .await + .expect("send readiness turn") + .expect("readiness response"); + assert_eq!(assistant_message_content(&ready), "SDK_REWIND_READY"); + assert_eq!( + std::fs::read_to_string(&file_path).expect("read prepared file"), + PREPARED_FILE_CONTENT + ); + let response = session .send_and_wait(format!( - "Use the create tool to create {FILE_NAME} containing exactly \ - {FILE_CONTENT}. After the tool succeeds, reply with exactly \ - SDK_REWIND_DONE." + "Use the edit tool to replace the exact contents of {FILE_NAME} from \ + {PREPARED_FILE_CONTENT} to {FILE_CONTENT}. After the tool succeeds, \ + reply with exactly SDK_REWIND_DONE." )) .await .expect("send rewind setup prompt") @@ -53,8 +66,9 @@ async fn should_restore_tracked_file_and_conversation() { let rewind_points = wait_for_rewind_points(&session).await; assert!(rewind_points.file_change_tracking_enabled); - assert_eq!(rewind_points.points.len(), 1); - let rewind_point = &rewind_points.points[0]; + assert_eq!(rewind_points.points.len(), 2); + let rewind_point = &rewind_points.points[1]; + assert!(rewind_point.turn_changed_files); assert!(rewind_point.can_restore_files); assert_eq!(rewind_point.file_count, 1); @@ -83,7 +97,10 @@ async fn should_restore_tracked_file_and_conversation() { assert!(rewind.events_removed.is_some_and(|count| count > 0)); assert_eq!(rewind.restored_files.len(), 1); assert_same_path(&file_path, Path::new(&rewind.restored_files[0])); - assert!(!file_path.exists()); + assert_eq!( + std::fs::read_to_string(&file_path).expect("read restored file"), + PREPARED_FILE_CONTENT + ); let events = session.get_events().await.expect("get events after rewind"); assert!(events.iter().all(|event| event.id != rewind_point.event_id)); @@ -108,10 +125,10 @@ async fn wait_for_rewind_points( .await .expect("list rewind points"); if result.unavailable_reason.is_none() - && result - .points - .first() - .is_some_and(|point| point.can_restore_files && point.file_count == 1) + && result.points.len() == 2 + && result.points[1].turn_changed_files + && result.points[1].can_restore_files + && result.points[1].file_count == 1 { return result; } diff --git a/rust/tests/e2e/rpc_session_state.rs b/rust/tests/e2e/rpc_session_state.rs index aa67312473..c723d10f5e 100644 --- a/rust/tests/e2e/rpc_session_state.rs +++ b/rust/tests/e2e/rpc_session_state.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use std::sync::Arc; +use async_trait::async_trait; use github_copilot_sdk::rpc::{ AuthInfoType, HistoryTruncateRequest, LspInitializeRequest, MetadataContextInfoRequest, MetadataRecomputeContextTokensRequest, MetadataRecordContextChangeRequest, @@ -16,8 +18,14 @@ use github_copilot_sdk::session_events::{ SessionTitleChangedData, SessionWorkspaceFileChangedData, ShutdownType, WorkspaceFileChangedOperation, }; +use github_copilot_sdk::tool::ToolHandler; +use github_copilot_sdk::{Error, Tool, ToolInvocation, ToolResult}; +use serde_json::json; +use tokio::sync::{Mutex, mpsc, oneshot}; -use super::support::{assistant_message_content, wait_for_condition, wait_for_event}; +use super::support::{ + assistant_message_content, recv_with_timeout, wait_for_condition, wait_for_event, +}; const MODEL_ID: &str = "claude-sonnet-4.5"; @@ -1111,8 +1119,28 @@ async fn should_report_processing_and_context_metadata() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; + let (entered_tx, mut entered_rx) = mpsc::unbounded_channel(); + let (release_tx, release_rx) = oneshot::channel(); let session = client - .create_session(ctx.approve_all_session_config().with_model(MODEL_ID)) + .create_session( + ctx.approve_all_session_config() + .with_model(MODEL_ID) + .with_tools(vec![ + Tool::new("processing_barrier") + .with_description( + "Blocks until the processing state has been observed", + ) + .with_parameters(json!({ + "type": "object", + "properties": {}, + "additionalProperties": false + })) + .with_handler(Arc::new(ProcessingBarrierTool { + entered_tx, + release_rx: Mutex::new(Some(release_rx)), + })), + ]), + ) .await .expect("create session"); @@ -1126,19 +1154,20 @@ async fn should_report_processing_and_context_metadata() { .processing ); session - .send("Reply with exactly: RUST_CONTEXT_INFO") + .send("Use processing_barrier, then reply with exactly: RUST_CONTEXT_INFO") .await .expect("send"); - wait_for_condition("session processing started", || async { + recv_with_timeout(&mut entered_rx, "processing barrier entry").await; + assert!( session .rpc() .metadata() .is_processing() .await - .expect("processing poll") + .expect("processing during tool call") .processing - }) - .await; + ); + release_tx.send(()).expect("release processing barrier"); wait_for_condition("session processing completed", || async { !session .rpc() @@ -1180,6 +1209,26 @@ async fn should_report_processing_and_context_metadata() { .await; } +struct ProcessingBarrierTool { + entered_tx: mpsc::UnboundedSender<()>, + release_rx: Mutex>>, +} + +#[async_trait] +impl ToolHandler for ProcessingBarrierTool { + async fn call(&self, _invocation: ToolInvocation) -> Result { + let _ = self.entered_tx.send(()); + self.release_rx + .lock() + .await + .take() + .expect("processing barrier called once") + .await + .expect("processing barrier released"); + Ok(ToolResult::Text("PROCESSING_OBSERVED".to_string())) + } +} + fn expect_err_contains(result: Result, expected: &str) { let err = match result { Ok(_) => panic!("expected error containing {expected:?}"), diff --git a/rust/tests/e2e/session.rs b/rust/tests/e2e/session.rs index e2ca76c478..8c2463a5fa 100644 --- a/rust/tests/e2e/session.rs +++ b/rust/tests/e2e/session.rs @@ -501,8 +501,7 @@ async fn should_abort_a_session() { #[tokio::test] async fn should_resume_a_session_using_the_same_client() { - super::support::with_shared_e2e_context( - &E2E, + super::support::with_dedicated_e2e_context( "session", "should_resume_a_session_using_the_same_client", |ctx| { @@ -1752,4 +1751,4 @@ fn secret_number_tool() -> Tool { .with_handler(Arc::new(SecretNumberTool)) } static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("session", 30); + super::support::SharedE2eGroup::standard("session", 29); diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index 76f9a21c8f..8a4161efef 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -510,12 +510,8 @@ impl E2eContext { .expect("start E2E client") } - /// Start a client that hosts the runtime in-process over FFI - /// ([`Transport::InProcess`]). Unlike the stdio harness, the CLI - /// entrypoint is passed as the program directly (the FFI host builds the - /// `node --embedded-host` argv itself and loads the sibling - /// runtime cdylib), so a `.js` entrypoint is not split into node + - /// prefix_args here. + /// Start a client that hosts the bundled runtime directly in-process over + /// FFI ([`Transport::InProcess`]). #[cfg_attr(not(feature = "bundled-in-process"), allow(dead_code))] pub async fn start_inprocess_client(&self) -> Client { let options = ClientOptions::new().with_transport(Transport::InProcess); @@ -1070,7 +1066,8 @@ impl InProcessEnvGuard { pairs.push(("COPILOT_SDK_AUTH_TOKEN".into(), "".into())); pairs.push(( "COPILOT_CLI_PATH".into(), - ctx.cli_path.clone().into_os_string(), + std::env::var_os("COPILOT_CLI_PATH") + .unwrap_or_else(|| ctx.cli_path.clone().into_os_string()), )); // Some tests opt into gated runtime APIs via per-client `options.env`, which the // in-process transport does not pass to the shared native runtime (see issue #1934). diff --git a/rust/tests/extension_launch_provider_runtime_test.rs b/rust/tests/extension_launch_provider_runtime_test.rs new file mode 100644 index 0000000000..f08d6af7f0 --- /dev/null +++ b/rust/tests/extension_launch_provider_runtime_test.rs @@ -0,0 +1,321 @@ +//! Real-boundary coverage for hostless extension discovery and lifecycle. +//! +//! Router tests can prove provider dispatch without exercising runtime session +//! setup, while an empty extension-list smoke test never requires the runtime +//! to install extension services. This test requires both boundaries and is +//! ignored until a real wrapper/runtime.node pair is supplied explicitly. + +#![cfg(unix)] +#![allow(clippy::unwrap_used)] + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use async_trait::async_trait; +use github_copilot_sdk::extension_launch_provider::{ + ExtensionLaunchProfile, ExtensionLaunchProvider, ExtensionLaunchProviderResolveRequest, + ExtensionLaunchProviderResolveResult, +}; +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::rpc::{ + ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, + ToolResult, ToolResultType, ToolsExecuteRequest, +}; +use github_copilot_sdk::{CliProgram, Client, ClientOptions, SessionConfig, Transport}; +use serde_json::{Value, json}; +use tokio::sync::mpsc; +use tokio::time::{sleep, timeout}; + +const TEST_TIMEOUT: Duration = Duration::from_secs(15); +const RUNTIME_PATH_ENV: &str = "COPILOT_RUNTIME_E2E_PATH"; + +struct RecordingProvider { + executable: PathBuf, + state_path: PathBuf, + requests: mpsc::UnboundedSender, +} + +#[async_trait] +impl ExtensionLaunchProvider for RecordingProvider { + async fn resolve( + &self, + request: ExtensionLaunchProviderResolveRequest, + ) -> github_copilot_sdk::Result { + self.requests.send(request.clone()).unwrap(); + Ok(ExtensionLaunchProviderResolveResult { + launch: Some(ExtensionLaunchProfile { + executable: self.executable.to_string_lossy().into_owned(), + args: Vec::new(), + env: HashMap::from([ + ("EXTENSION_PATH".to_string(), request.module_path.clone()), + ( + "FIXTURE_STATE_PATH".to_string(), + self.state_path.to_string_lossy().into_owned(), + ), + ("COPILOT_AUTO_UPDATE".to_string(), "false".to_string()), + ( + "COPILOT_SDK_PATH".to_string(), + "provider-must-not-win".to_string(), + ), + ( + "SESSION_ID".to_string(), + "provider-must-not-win".to_string(), + ), + ( + "COPILOT_EXTENSION_PARENT_PID".to_string(), + "provider-must-not-win".to_string(), + ), + ]), + }), + }) + } +} + +#[tokio::test(flavor = "multi_thread")] +#[ignore = "requires COPILOT_RUNTIME_E2E_PATH to name a real wrapper/runtime.node pair"] +async fn real_wrapper_installs_and_runs_hostless_extensions() { + let runtime_path = std::env::var_os(RUNTIME_PATH_ENV) + .map(PathBuf::from) + .expect("COPILOT_RUNTIME_E2E_PATH must name a copilot-runtime executable"); + assert_runtime_pair(&runtime_path); + + let home = tempfile::tempdir().unwrap(); + let workspace = tempfile::tempdir().unwrap(); + let extension_dir = workspace + .path() + .join(".github") + .join("extensions") + .join("lifecycle"); + std::fs::create_dir_all(&extension_dir).unwrap(); + let module_path = extension_dir.join("extension.mjs"); + std::fs::write(&module_path, "export default {};\n").unwrap(); + let module_path = std::fs::canonicalize(module_path).unwrap(); + let sdk_path = home.path().join("extension-sdk"); + std::fs::create_dir_all(&sdk_path).unwrap(); + let sdk_path = std::fs::canonicalize(sdk_path).unwrap(); + let state_path = home.path().join("extension-state.jsonl"); + let (request_tx, mut request_rx) = mpsc::unbounded_channel(); + + let options = ClientOptions::new() + .with_program(CliProgram::Path(runtime_path)) + .with_transport(Transport::Stdio) + .with_cwd(workspace.path()) + .with_base_directory(home.path()) + .with_use_logged_in_user(false) + .with_env_remove(["COPILOT_CLI_DIST_DIR"]) + .with_extension_launch_provider(RecordingProvider { + executable: PathBuf::from(env!("CARGO_BIN_EXE_copilot-extension-test-fixture")), + state_path: state_path.clone(), + requests: request_tx, + }); + let client = Client::start(options).await.unwrap(); + let session = client + .create_session( + SessionConfig::default() + .with_request_extensions(true) + .with_extension_sdk_path(sdk_path.to_string_lossy()) + .with_permission_handler(Arc::new(ApproveAllHandler)), + ) + .await + .unwrap(); + + let first_request = recv_request(&mut request_rx).await; + assert_request(&first_request, &module_path); + let first_start = wait_for_starts(&state_path, 1).await.remove(0); + assert_runtime_owned_environment(&first_start, session.id(), &sdk_path, &module_path); + let first_pid = json_pid(&first_start); + let wrapper_pid = json_parent_pid(&first_start); + assert_eq!(direct_child_pids(wrapper_pid), vec![first_pid]); + + let listed = session.rpc().extensions().list().await.unwrap(); + assert_eq!(listed.extensions.len(), 1); + assert_eq!(listed.extensions[0].id, "project:lifecycle"); + assert_eq!(listed.extensions[0].status, ExtensionStatus::Running); + + let result = session + .rpc() + .tools() + .execute(ToolsExecuteRequest { + arguments: json!({ "text": "sdk-boundary" }), + name: "fixture_echo".to_string(), + tool_call_id: Some("fixture-call".to_string()), + }) + .await + .unwrap(); + match result { + ToolResult::String(value) => assert_eq!(value, "echoed"), + ToolResult::ToolResultExpanded(result) => { + assert_eq!(result.text_result_for_llm, "echoed"); + assert_eq!(result.result_type, ToolResultType::Success); + } + } + wait_for_invocations(&state_path, 1).await; + + session + .rpc() + .extensions() + .disable(ExtensionsDisableRequest { + id: "project:lifecycle".to_string(), + }) + .await + .unwrap(); + wait_for_process_exit(first_pid).await; + let disabled = session.rpc().extensions().list().await.unwrap(); + assert_eq!(disabled.extensions[0].status, ExtensionStatus::Disabled); + + session + .rpc() + .extensions() + .enable(ExtensionsEnableRequest { + id: "project:lifecycle".to_string(), + }) + .await + .unwrap(); + let second_request = recv_request(&mut request_rx).await; + assert_request(&second_request, &module_path); + let starts = wait_for_starts(&state_path, 2).await; + let second_pid = json_pid(&starts[1]); + assert_ne!(second_pid, first_pid); + assert!(process_exists(second_pid)); + assert_eq!(json_parent_pid(&starts[1]), wrapper_pid); + assert_eq!(direct_child_pids(wrapper_pid), vec![second_pid]); + let enabled = session.rpc().extensions().list().await.unwrap(); + assert_eq!(enabled.extensions[0].status, ExtensionStatus::Running); + + client.stop().await.unwrap(); + wait_for_process_exit(second_pid).await; + wait_for_process_exit(wrapper_pid).await; + assert!(request_rx.try_recv().is_err()); +} + +fn assert_runtime_pair(runtime_path: &Path) { + assert!(runtime_path.is_file(), "missing {}", runtime_path.display()); + let runtime_node = runtime_path.parent().unwrap().join("runtime.node"); + assert!(runtime_node.is_file(), "missing {}", runtime_node.display()); +} + +async fn recv_request( + requests: &mut mpsc::UnboundedReceiver, +) -> ExtensionLaunchProviderResolveRequest { + timeout(TEST_TIMEOUT, requests.recv()) + .await + .expect( + "runtime never invoked extensionLaunchProvider.resolve; hostless extension services \ + may not be installed", + ) + .expect("provider request channel closed") +} + +fn assert_request(request: &ExtensionLaunchProviderResolveRequest, module_path: &Path) { + assert_eq!(request.id, "project:lifecycle"); + assert_eq!(request.name, "lifecycle"); + assert_eq!(request.source, ExtensionSource::Project); + assert_eq!(Path::new(&request.module_path), module_path); +} + +fn read_state(path: &Path) -> Vec { + let Ok(contents) = std::fs::read_to_string(path) else { + return Vec::new(); + }; + contents + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect() +} + +async fn wait_for_starts(path: &Path, count: usize) -> Vec { + wait_for_state(path, "start", count).await +} + +async fn wait_for_invocations(path: &Path, count: usize) -> Vec { + wait_for_state(path, "invoke", count).await +} + +async fn wait_for_state(path: &Path, kind: &str, count: usize) -> Vec { + let deadline = Instant::now() + TEST_TIMEOUT; + loop { + let matching: Vec<_> = read_state(path) + .into_iter() + .filter(|entry| entry["kind"] == kind) + .collect(); + if matching.len() >= count { + return matching; + } + assert!( + Instant::now() < deadline, + "timed out waiting for {count} {kind} entries; state: {:?}", + read_state(path) + ); + sleep(Duration::from_millis(25)).await; + } +} + +fn assert_runtime_owned_environment( + start: &Value, + session_id: &str, + sdk_path: &Path, + module_path: &Path, +) { + assert_eq!(start["sessionId"], session_id); + assert_eq!( + start["sdkPath"].as_str(), + Some(sdk_path.to_string_lossy().as_ref()) + ); + assert_eq!( + start["extensionPath"].as_str(), + Some(module_path.to_string_lossy().as_ref()) + ); + assert_eq!(start["autoUpdate"], "false"); + assert!(start["cliDistDir"].is_null()); + let parent_pid = start["parentPid"].as_str().unwrap(); + assert_ne!(parent_pid, "provider-must-not-win"); + assert!(parent_pid.parse::().is_ok()); +} + +fn json_pid(value: &Value) -> u32 { + value["pid"].as_u64().unwrap() as u32 +} + +fn json_parent_pid(value: &Value) -> u32 { + value["parentPid"].as_str().unwrap().parse().unwrap() +} + +fn direct_child_pids(parent_pid: u32) -> Vec { + let output = Command::new("ps") + .args(["-axo", "pid=,ppid="]) + .output() + .expect("list processes"); + assert!(output.status.success(), "ps failed: {output:?}"); + let mut children: Vec<_> = String::from_utf8(output.stdout) + .unwrap() + .lines() + .filter_map(|line| { + let mut columns = line.split_whitespace(); + let pid = columns.next()?.parse::().ok()?; + let ppid = columns.next()?.parse::().ok()?; + (ppid == parent_pid).then_some(pid) + }) + .collect(); + children.sort_unstable(); + children +} + +fn process_exists(pid: u32) -> bool { + Command::new("kill") + .args(["-0", &pid.to_string()]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()) +} + +async fn wait_for_process_exit(pid: u32) { + let deadline = Instant::now() + TEST_TIMEOUT; + while process_exists(pid) { + assert!(Instant::now() < deadline, "process {pid} did not exit"); + sleep(Duration::from_millis(25)).await; + } +} diff --git a/rust/tests/extension_launch_provider_test.rs b/rust/tests/extension_launch_provider_test.rs new file mode 100644 index 0000000000..b1a9592133 --- /dev/null +++ b/rust/tests/extension_launch_provider_test.rs @@ -0,0 +1,493 @@ +#![allow(clippy::unwrap_used)] + +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::extension_launch_provider::{ + ExtensionLaunchProfile, ExtensionLaunchProvider, ExtensionLaunchProviderResolveRequest, + ExtensionLaunchProviderResolveResult, +}; +use github_copilot_sdk::rpc::ExtensionSource; +use github_copilot_sdk::{CliProgram, Client, ClientOptions, Error, ErrorKind, Transport}; +use serde_json::{Value, json}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, duplex}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::{Notify, mpsc}; +use tokio::time::timeout; + +const TEST_TIMEOUT: Duration = Duration::from_secs(2); + +async fn write_framed(writer: &mut (impl AsyncWrite + Unpin), value: &Value) { + let body = serde_json::to_vec(value).unwrap(); + let header = format!("Content-Length: {}\r\n\r\n", body.len()); + writer.write_all(header.as_bytes()).await.unwrap(); + writer.write_all(&body).await.unwrap(); + writer.flush().await.unwrap(); +} + +async fn read_framed(reader: &mut (impl AsyncRead + Unpin)) -> Option { + let mut header = String::new(); + loop { + let mut byte = [0u8; 1]; + match reader.read_exact(&mut byte).await { + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => return None, + Err(error) => panic!("failed to read frame header: {error}"), + } + header.push(byte[0] as char); + if header.ends_with("\r\n\r\n") { + break; + } + } + + let length = header + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut body = vec![0; length]; + reader.read_exact(&mut body).await.unwrap(); + Some(serde_json::from_slice(&body).unwrap()) +} + +fn resolve_params() -> Value { + json!({ + "id": "project:legacy-extension", + "modulePath": "/extensions/legacy/index.js", + "name": "Legacy extension", + "source": "project" + }) +} + +fn app_launch_result(executable: &str) -> ExtensionLaunchProviderResolveResult { + ExtensionLaunchProviderResolveResult { + launch: Some(ExtensionLaunchProfile { + executable: executable.to_string(), + args: vec!["/app/preloads/extension_bootstrap.mjs".to_string()], + env: HashMap::from([ + ("COPILOT_AUTO_UPDATE".to_string(), "false".to_string()), + ( + "EXTENSION_PATH".to_string(), + "/extensions/legacy/index.js".to_string(), + ), + ]), + }), + } +} + +struct AppProvider { + executable: String, + observed: Option>, + release: Option>, + calls: Arc, +} + +#[async_trait] +impl ExtensionLaunchProvider for AppProvider { + async fn resolve( + &self, + request: ExtensionLaunchProviderResolveRequest, + ) -> github_copilot_sdk::Result { + self.calls.fetch_add(1, Ordering::SeqCst); + if let Some(observed) = &self.observed { + observed.send(request).unwrap(); + } + if let Some(release) = &self.release { + release.notified().await; + } + Ok(app_launch_result(&self.executable)) + } +} + +struct FailingProvider; + +#[async_trait] +impl ExtensionLaunchProvider for FailingProvider { + async fn resolve( + &self, + _request: ExtensionLaunchProviderResolveRequest, + ) -> github_copilot_sdk::Result { + Err(Error::with_message( + ErrorKind::InvalidConfig, + "extension profile lookup failed", + )) + } +} + +#[test] +fn launch_profile_request_and_result_serialize_exactly() { + let request: ExtensionLaunchProviderResolveRequest = + serde_json::from_value(resolve_params()).unwrap(); + assert_eq!(request.id, "project:legacy-extension"); + assert_eq!(request.module_path, "/extensions/legacy/index.js"); + assert_eq!(request.name, "Legacy extension"); + assert_eq!(request.source, ExtensionSource::Project); + assert_eq!(serde_json::to_value(request).unwrap(), resolve_params()); + + let result = app_launch_result("/app/copilot"); + assert_eq!( + serde_json::to_value(&result).unwrap(), + json!({ + "launch": { + "executable": "/app/copilot", + "args": ["/app/preloads/extension_bootstrap.mjs"], + "env": { + "COPILOT_AUTO_UPDATE": "false", + "EXTENSION_PATH": "/extensions/legacy/index.js" + } + } + }) + ); + + let round_trip: ExtensionLaunchProviderResolveResult = + serde_json::from_value(serde_json::to_value(result).unwrap()).unwrap(); + let launch = round_trip.launch.unwrap(); + assert_eq!(launch.executable, "/app/copilot"); + assert_eq!(launch.args, vec!["/app/preloads/extension_bootstrap.mjs"]); + assert_eq!( + launch.env, + HashMap::from([ + ("COPILOT_AUTO_UPDATE".to_string(), "false".to_string()), + ( + "EXTENSION_PATH".to_string(), + "/extensions/legacy/index.js".to_string() + ) + ]) + ); +} + +#[tokio::test] +async fn configured_async_provider_dispatches_without_a_session() { + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let temp = tempfile::tempdir().unwrap(); + let (observed_tx, mut observed_rx) = mpsc::unbounded_channel(); + let release = Arc::new(Notify::new()); + let calls = Arc::new(AtomicUsize::new(0)); + let client = Client::from_streams_with_extension_launch_provider( + client_read, + client_write, + temp.path().to_path_buf(), + Arc::new(AppProvider { + executable: "/app/copilot".to_string(), + observed: Some(observed_tx), + release: Some(release.clone()), + calls: calls.clone(), + }), + ) + .unwrap(); + client.start_router_for_test(); + + write_framed( + &mut server_write, + &json!({ + "jsonrpc": "2.0", + "id": 41, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + + let observed = timeout(TEST_TIMEOUT, observed_rx.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(observed.id, "project:legacy-extension"); + assert!( + timeout(Duration::from_millis(25), read_framed(&mut server_read)) + .await + .is_err() + ); + + release.notify_one(); + let response = timeout(TEST_TIMEOUT, read_framed(&mut server_read)) + .await + .unwrap() + .unwrap(); + assert_eq!(response["id"], 41); + assert_eq!( + response["result"], + serde_json::to_value(app_launch_result("/app/copilot")).unwrap() + ); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn missing_provider_returns_client_global_handler_error() { + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let temp = tempfile::tempdir().unwrap(); + let client = + Client::from_streams(client_read, client_write, temp.path().to_path_buf()).unwrap(); + client.start_router_for_test(); + + write_framed( + &mut server_write, + &json!({ + "jsonrpc": "2.0", + "id": 42, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + + let response = timeout(TEST_TIMEOUT, read_framed(&mut server_read)) + .await + .unwrap() + .unwrap(); + assert_eq!(response["id"], 42); + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + "No extensionLaunchProvider client-global handler registered" + ); +} + +#[tokio::test] +async fn provider_error_is_returned_as_json_rpc_error() { + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let temp = tempfile::tempdir().unwrap(); + let client = Client::from_streams_with_extension_launch_provider( + client_read, + client_write, + temp.path().to_path_buf(), + Arc::new(FailingProvider), + ) + .unwrap(); + client.start_router_for_test(); + + write_framed( + &mut server_write, + &json!({ + "jsonrpc": "2.0", + "id": 43, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + + let response = timeout(TEST_TIMEOUT, read_framed(&mut server_read)) + .await + .unwrap() + .unwrap(); + assert_eq!(response["id"], 43); + assert_eq!(response["error"]["code"], -32603); + assert!( + response["error"]["message"] + .as_str() + .unwrap() + .contains("extension profile lookup failed") + ); +} + +async fn respond_to_connect( + reader: &mut (impl AsyncRead + Unpin), + writer: &mut (impl AsyncWrite + Unpin), +) { + let request = read_framed(reader).await.unwrap(); + assert_eq!(request["method"], "connect"); + write_framed( + writer, + &json!({ + "jsonrpc": "2.0", + "id": request["id"], + "result": { + "ok": true, + "protocolVersion": 3, + "version": "test" + } + }), + ) + .await; +} + +async fn read_registration(reader: &mut (impl AsyncRead + Unpin)) -> Value { + let request = read_framed(reader).await.unwrap(); + assert_eq!(request["method"], "registerExtensionLaunchProvider"); + assert_eq!(request["params"], json!({})); + request +} + +fn external_options(port: u16, provider: AppProvider) -> ClientOptions { + ClientOptions::new() + .with_program(CliProgram::Path("unused-for-external-transport".into())) + .with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port, + connection_token: None, + }) + .with_extension_launch_provider(provider) +} + +#[tokio::test] +async fn registration_precedes_session_work_and_routes_during_start() { + let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let calls = Arc::new(AtomicUsize::new(0)); + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let (mut reader, mut writer) = stream.into_split(); + respond_to_connect(&mut reader, &mut writer).await; + let registration = read_registration(&mut reader).await; + + write_framed( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": 501, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + let callback_response = read_framed(&mut reader).await.unwrap(); + assert_eq!(callback_response["id"], 501); + assert_eq!( + callback_response["result"], + serde_json::to_value(app_launch_result("/app/copilot")).unwrap() + ); + + write_framed( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": registration["id"], + "result": null + }), + ) + .await; + assert!( + timeout(Duration::from_millis(50), read_framed(&mut reader)) + .await + .is_err() + ); + }); + + let client = Client::start(external_options( + port, + AppProvider { + executable: "/app/copilot".to_string(), + observed: None, + release: None, + calls: calls.clone(), + }, + )) + .await + .unwrap(); + + timeout(TEST_TIMEOUT, server).await.unwrap().unwrap(); + assert_eq!(calls.load(Ordering::SeqCst), 1); + client.stop().await.unwrap(); +} + +async fn serve_registered_client(stream: TcpStream, request_id: u64, expected_executable: &str) { + let (mut reader, mut writer) = stream.into_split(); + respond_to_connect(&mut reader, &mut writer).await; + let registration = read_registration(&mut reader).await; + write_framed( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": registration["id"], + "result": null + }), + ) + .await; + + write_framed( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": request_id, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + let response = read_framed(&mut reader).await.unwrap(); + assert_eq!(response["id"], request_id); + assert_eq!( + response["result"]["launch"]["executable"], + expected_executable + ); + assert_eq!(read_framed(&mut reader).await, None); +} + +#[tokio::test] +async fn shutdown_and_restart_do_not_reuse_or_duplicate_providers() { + let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let first_calls = Arc::new(AtomicUsize::new(0)); + let second_calls = Arc::new(AtomicUsize::new(0)); + let (served_tx, mut served_rx) = mpsc::unbounded_channel(); + + let server = tokio::spawn(async move { + let (first, _) = listener.accept().await.unwrap(); + serve_registered_client(first, 601, "/app/first-copilot").await; + served_tx.send(()).unwrap(); + + let (second, _) = listener.accept().await.unwrap(); + serve_registered_client(second, 602, "/app/second-copilot").await; + served_tx.send(()).unwrap(); + }); + + let first = Client::start(external_options( + port, + AppProvider { + executable: "/app/first-copilot".to_string(), + observed: None, + release: None, + calls: first_calls.clone(), + }, + )) + .await + .unwrap(); + timeout(TEST_TIMEOUT, async { + while first_calls.load(Ordering::SeqCst) == 0 { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + first.stop().await.unwrap(); + timeout(TEST_TIMEOUT, served_rx.recv()) + .await + .unwrap() + .unwrap(); + + let second = Client::start(external_options( + port, + AppProvider { + executable: "/app/second-copilot".to_string(), + observed: None, + release: None, + calls: second_calls.clone(), + }, + )) + .await + .unwrap(); + timeout(TEST_TIMEOUT, async { + while second_calls.load(Ordering::SeqCst) == 0 { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + second.stop().await.unwrap(); + timeout(TEST_TIMEOUT, served_rx.recv()) + .await + .unwrap() + .unwrap(); + timeout(TEST_TIMEOUT, server).await.unwrap().unwrap(); + + assert_eq!(first_calls.load(Ordering::SeqCst), 1); + assert_eq!(second_calls.load(Ordering::SeqCst), 1); +} diff --git a/rust/tests/fixtures/extension_fixture.rs b/rust/tests/fixtures/extension_fixture.rs new file mode 100644 index 0000000000..787c82503a --- /dev/null +++ b/rust/tests/fixtures/extension_fixture.rs @@ -0,0 +1,127 @@ +use std::fs::OpenOptions; +use std::io::{BufRead, BufReader, Write}; + +use serde_json::{Value, json}; + +fn main() { + let session_id = std::env::var("SESSION_ID").expect("SESSION_ID"); + record_state(json!({ + "kind": "start", + "pid": std::process::id(), + "sessionId": session_id, + "sdkPath": std::env::var("COPILOT_SDK_PATH").ok(), + "parentPid": std::env::var("COPILOT_EXTENSION_PARENT_PID").ok(), + "extensionPath": std::env::var("EXTENSION_PATH").ok(), + "autoUpdate": std::env::var("COPILOT_AUTO_UPDATE").ok(), + "cliDistDir": std::env::var("COPILOT_CLI_DIST_DIR").ok(), + })); + + let stdin = std::io::stdin(); + let mut reader = BufReader::new(stdin.lock()); + let stdout = std::io::stdout(); + let mut writer = stdout.lock(); + write_message( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "session.resume", + "params": { + "sessionId": session_id, + "tools": [{ + "name": "fixture_echo", + "description": "Returns a deterministic fixture result", + "parameters": { + "type": "object", + "properties": { "text": { "type": "string" } }, + "required": ["text"] + } + }], + "requestPermission": false, + "enableConfigDiscovery": false, + "disableResume": true, + "streaming": true + } + }), + ); + + let mut next_id = 2_u64; + while let Some(message) = read_message(&mut reader) { + if message.get("id") == Some(&json!(1)) { + assert!( + message.get("error").is_none(), + "extension resume failed: {message}" + ); + continue; + } + if message.pointer("/params/event/type") != Some(&json!("external_tool.requested")) { + continue; + } + let Some(request_id) = message + .pointer("/params/event/data/requestId") + .and_then(Value::as_str) + else { + continue; + }; + record_state(json!({ + "kind": "invoke", + "pid": std::process::id(), + "requestId": request_id, + })); + write_message( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": next_id, + "method": "session.tools.handlePendingToolCall", + "params": { + "sessionId": session_id, + "requestId": request_id, + "result": "echoed" + } + }), + ); + next_id += 1; + } +} + +fn record_state(value: Value) { + let path = std::env::var("FIXTURE_STATE_PATH").expect("FIXTURE_STATE_PATH"); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(path) + .expect("open fixture state"); + writeln!(file, "{value}").expect("write fixture state"); +} + +fn write_message(writer: &mut impl Write, value: &Value) { + let body = serde_json::to_vec(value).expect("serialize fixture message"); + write!(writer, "Content-Length: {}\r\n\r\n", body.len()).expect("write fixture header"); + writer.write_all(&body).expect("write fixture body"); + writer.flush().expect("flush fixture message"); +} + +fn read_message(reader: &mut impl BufRead) -> Option { + let mut content_length = None; + loop { + let mut line = String::new(); + if reader.read_line(&mut line).expect("read fixture header") == 0 { + return None; + } + if line == "\r\n" { + break; + } + if let Some(value) = line.strip_prefix("Content-Length:") { + content_length = Some( + value + .trim() + .parse::() + .expect("parse fixture content length"), + ); + } + } + let mut body = vec![0; content_length.expect("fixture content length")]; + reader.read_exact(&mut body).expect("read fixture body"); + Some(serde_json::from_slice(&body).expect("parse fixture message")) +} diff --git a/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml b/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml index 2ef3733e00..8a099ec46a 100644 --- a/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml +++ b/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml @@ -5,17 +5,33 @@ conversations: - role: system content: ${system} - role: user - content: Use the create tool to create rewind-sdk.txt containing exactly SDK rewind content. After the tool succeeds, - reply with exactly SDK_REWIND_DONE. + content: Use the edit tool to replace the exact contents of rewind-sdk.txt from Original rewind content to Prepared + rewind content. After the tool succeeds, reply with exactly SDK_REWIND_READY. - role: assistant tool_calls: - id: toolcall_0 type: function function: - name: create - arguments: '{"path":"${workdir}/rewind-sdk.txt","file_text":"SDK rewind content"}' + name: edit + arguments: '{"path":"${workdir}/rewind-sdk.txt","old_str":"Original rewind content","new_str":"Prepared rewind + content"}' - role: tool tool_call_id: toolcall_0 - content: Created file ${workdir}/rewind-sdk.txt with 18 characters + content: File ${workdir}/rewind-sdk.txt updated with changes. + - role: assistant + content: SDK_REWIND_READY + - role: user + content: Use the edit tool to replace the exact contents of rewind-sdk.txt from Prepared rewind content to SDK rewind + content. After the tool succeeds, reply with exactly SDK_REWIND_DONE. + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: edit + arguments: '{"path":"${workdir}/rewind-sdk.txt","old_str":"Prepared rewind content","new_str":"SDK rewind content"}' + - role: tool + tool_call_id: toolcall_1 + content: File ${workdir}/rewind-sdk.txt updated with changes. - role: assistant content: SDK_REWIND_DONE diff --git a/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml b/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml index 6760888d7b..d79fd22384 100644 --- a/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml +++ b/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml @@ -5,6 +5,16 @@ conversations: - role: system content: ${system} - role: user - content: "Reply with exactly: RUST_CONTEXT_INFO" + content: "Use processing_barrier, then reply with exactly: RUST_CONTEXT_INFO" + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: processing_barrier + arguments: "{}" + - role: tool + tool_call_id: toolcall_0 + content: PROCESSING_OBSERVED - role: assistant content: RUST_CONTEXT_INFO From 5a2a857bd711527cc7d7f0ef4bc545cc99e88d7f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 1 Sep 2026 08:56:08 +0000 Subject: [PATCH 12/60] docs: update version references to 1.0.13-preview.4 --- java/README.md | 8 ++++---- java/sdk/jbang-example.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java/README.md b/java/README.md index 77b68bdf75..f3ba3f63a0 100644 --- a/java/README.md +++ b/java/README.md @@ -36,14 +36,14 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. com.github copilot-sdk-java - 1.0.13-preview.3 + 1.0.13-preview.4 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.13-preview.3' +implementation 'com.github:copilot-sdk-java:1.0.13-preview.4' ``` #### Snapshot Builds @@ -62,7 +62,7 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.14-preview.3-SNAPSHOT + 1.0.14-preview.4-SNAPSHOT ``` @@ -71,7 +71,7 @@ Snapshot builds of the next development version are published to Maven Central S Replace `${copilot.sdk.version}` with the latest release from Maven Central. ```groovy -implementation 'com.github:copilot-sdk-java:1.0.14-preview.3-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.14-preview.4-SNAPSHOT' ``` ## In-process mode (experimental) diff --git a/java/sdk/jbang-example.java b/java/sdk/jbang-example.java index afff013803..0a1505f9c2 100644 --- a/java/sdk/jbang-example.java +++ b/java/sdk/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.13-preview.3 +//DEPS com.github:copilot-sdk-java:1.0.13-preview.4 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; From fc78bf6cbb114814141d936099190e320b55a89c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 1 Sep 2026 08:56:41 +0000 Subject: [PATCH 13/60] [maven-release-plugin] prepare release java/v1.0.13-preview.4 --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 21629a33f9..9ee2c1850f 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.3-SNAPSHOT + 1.0.13-preview.4 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.4 diff --git a/java/pom.xml b/java/pom.xml index bd317cd197..40d175edc7 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.3-SNAPSHOT + 1.0.13-preview.4 pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.4 diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index 46ae52e487..67a326ed11 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.3-SNAPSHOT + 1.0.13-preview.4 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.4 From 5b3a03e2d5615076598b9421cd2bced93f4612e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 1 Sep 2026 08:56:46 +0000 Subject: [PATCH 14/60] [maven-release-plugin] prepare for next development iteration --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 9ee2c1850f..c03be9909e 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.4 + 1.0.14-preview.4-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.4 + HEAD diff --git a/java/pom.xml b/java/pom.xml index 40d175edc7..91a97aadff 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.4 + 1.0.14-preview.4-SNAPSHOT pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.4 + HEAD diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index 67a326ed11..bb7cd2fa36 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.4 + 1.0.14-preview.4-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.4 + HEAD From 759156077163205bbb7235c3467856ed69de9d10 Mon Sep 17 00:00:00 2001 From: Quim Muntal Date: Tue, 1 Sep 2026 13:47:45 +0000 Subject: [PATCH 15/60] [go] Auto-detect bundler package name (#2452) * [go] Auto-detect bundler package name * [go] Respect target constraints in package detection --- go/cmd/bundler/main.go | 73 ++++++++++++++++++++++++++++++++++--- go/cmd/bundler/main_test.go | 56 ++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 5 deletions(-) diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index fb11ff8699..e7bbd7a64e 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -19,6 +19,9 @@ import ( "encoding/json" "flag" "fmt" + "go/build" + "go/parser" + "go/token" "io" "net/http" "os" @@ -33,10 +36,11 @@ import ( const ( // Keep these URLs centralized so reviewers can verify all outbound calls in one place. - sdkModule = "github.com/github/copilot-sdk/go" - packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" - tarballURLFmt = "https://registry.npmjs.org/@github/copilot-%s/-/copilot-%s-%s.tgz" - licenseTarballFmt = "https://registry.npmjs.org/@github/copilot/-/copilot-%s.tgz" + sdkModule = "github.com/github/copilot-sdk/go" + packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" + tarballURLFmt = "https://registry.npmjs.org/@github/copilot-%s/-/copilot-%s-%s.tgz" + licenseTarballFmt = "https://registry.npmjs.org/@github/copilot/-/copilot-%s.tgz" + defaultPackageName = "main" ) // Platform info: npm package suffix, binary name @@ -89,6 +93,11 @@ func main() { return } + pkgName, err := detectPackageName(*output, goos, goarch) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to detect package name: %v; using package %s\n", err, pkgName) + } + fmt.Printf("Building bundle for %s (CLI version %s)\n", *platform, version) bundle, err := buildBundle(info, version, outputPath, goos) @@ -137,7 +146,7 @@ func main() { muslBundle.wrapperHash, muslBundle.assetsArtifactPath, muslBundle.assetsHash, - "main", + pkgName, ); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) @@ -193,6 +202,60 @@ func validPlatforms() []string { return result } +// detectPackageName reads package clauses from files that match the target +// platform and build constraints. It returns defaultPackageName with an error +// when detection fails. +func detectPackageName(dir, goos, goarch string) (string, error) { + if dir == "" { + dir = "." + } + + entries, err := os.ReadDir(dir) + if err != nil { + return defaultPackageName, fmt.Errorf("failed to read package directory %q: %w", dir, err) + } + + buildContext := build.Default + buildContext.GOOS = goos + buildContext.GOARCH = goarch + + packageName := "" + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || + strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") || + strings.HasSuffix(name, "_test.go") || strings.HasPrefix(name, "zcopilot_") { + continue + } + matches, err := buildContext.MatchFile(dir, name) + if err != nil { + return defaultPackageName, fmt.Errorf("failed to evaluate build constraints in %q: %w", filepath.Join(dir, name), err) + } + if !matches { + continue + } + + path := filepath.Join(dir, name) + file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.PackageClauseOnly) + if err != nil { + return defaultPackageName, fmt.Errorf("failed to parse package clause in %q: %w", path, err) + } + + if packageName == "" { + packageName = file.Name.Name + continue + } + if packageName != file.Name.Name { + return defaultPackageName, fmt.Errorf("multiple packages %q and %q found in %q", packageName, file.Name.Name, dir) + } + } + + if packageName == "" { + return defaultPackageName, fmt.Errorf("no Go package found in %q", dir) + } + return packageName, nil +} + // detectCLIVersion detects the CLI version by: // 1. Running "go list -m" to get the copilot-sdk version from the user's go.mod // 2. Fetching the package-lock.json from the SDK repo at that version diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go index 0a7f1c3876..f41afe43f5 100644 --- a/go/cmd/bundler/main_test.go +++ b/go/cmd/bundler/main_test.go @@ -101,6 +101,62 @@ func readTarGz(t *testing.T, path string) map[string]string { } } +func TestDetectPackageName(t *testing.T) { + dir := t.TempDir() + files := map[string]string{ + "app_linux.go": "package application\n", + "app_test.go": "package application_test\n", + "app_windows.go": "package windowsapplication\n", + "tagged.go": "//go:build windows\n\npackage windowsapplication\n", + "zcopilot_linux_amd64.go": "package main\n", + "_ignored.go": "package ignored\n", + "zcopilot_inprocess_linux.go": "package main\n", + } + for name, content := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { + t.Fatal(err) + } + } + + for _, test := range []struct { + goos string + want string + }{ + {goos: "linux", want: "application"}, + {goos: "windows", want: "windowsapplication"}, + } { + t.Run(test.goos, func(t *testing.T) { + got, err := detectPackageName(dir, test.goos, "amd64") + if err != nil { + t.Fatal(err) + } + if got != test.want { + t.Fatalf("detectPackageName() = %q, want %q", got, test.want) + } + }) + } +} + +func TestDetectPackageNameFallsBackForMultiplePackages(t *testing.T) { + dir := t.TempDir() + for name, content := range map[string]string{ + "one.go": "package one\n", + "two.go": "package two\n", + } { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { + t.Fatal(err) + } + } + + got, err := detectPackageName(dir, "linux", "amd64") + if err == nil { + t.Fatal("detectPackageName() succeeded for a directory containing multiple packages") + } + if got != defaultPackageName { + t.Fatalf("detectPackageName() = %q, want fallback %q", got, defaultPackageName) + } +} + func TestGenerateGoFileEmbedsRuntimeWrapperPair(t *testing.T) { dir := t.TempDir() binaryPath := filepath.Join(dir, "copilot.zst") From 80d09ff1660cd7be1be65a0045b82d71cd8841e7 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Tue, 1 Sep 2026 16:15:16 +0000 Subject: [PATCH 16/60] Expose session feature flag overrides across SDKs (#2451) * Expose Rust session feature flag overrides Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9b0b5b94-f94a-4df4-8e57-6d7ed6fe04c1 * Expose session feature flags across SDKs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document Python feature flag options Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson Copilot-Session: 9b0b5b94-f94a-4df4-8e57-6d7ed6fe04c1 --- dotnet/src/Client.cs | 4 ++ dotnet/src/Types.cs | 9 +++ dotnet/test/Unit/SerializationTests.cs | 40 +++++++++++ go/client.go | 6 ++ go/client_test.go | 56 +++++++++++++++ go/types.go | 8 +++ .../github/copilot/SessionRequestBuilder.java | 2 + .../copilot/rpc/CreateSessionRequest.java | 13 ++++ .../copilot/rpc/ResumeSessionConfig.java | 19 ++++++ .../copilot/rpc/ResumeSessionRequest.java | 13 ++++ .../com/github/copilot/rpc/SessionConfig.java | 19 ++++++ .../copilot/SessionRequestBuilderTest.java | 20 ++++++ nodejs/src/client.ts | 2 + nodejs/src/types.ts | 6 ++ nodejs/test/client.test.ts | 33 +++++++++ python/copilot/client.py | 13 ++++ python/test_client.py | 35 ++++++++++ rust/src/types.rs | 68 +++++++++++++++++++ rust/src/wire.rs | 5 ++ 19 files changed, 371 insertions(+) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 092112cebc..141473f535 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1285,6 +1285,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance Providers: config.Providers, Models: config.Models, ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, + FeatureFlags: config.FeatureFlags, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, GitHubMcpToolConfig: config.GitHubMcpToolConfig, @@ -1526,6 +1527,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes Providers: config.Providers, Models: config.Models, ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, + FeatureFlags: config.FeatureFlags, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, GitHubMcpToolConfig: config.GitHubMcpToolConfig, @@ -3021,6 +3023,7 @@ internal record CreateSessionRequest( IList? Providers = null, IList? Models = null, OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, + [property: JsonPropertyName("featureFlags")] IDictionary? FeatureFlags = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, @@ -3139,6 +3142,7 @@ internal record ResumeSessionRequest( IList? Providers = null, IList? Models = null, OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, + [property: JsonPropertyName("featureFlags")] IDictionary? FeatureFlags = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 6cf02d2a73..a7de7d85e4 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -3225,6 +3225,9 @@ protected SessionConfigBase(SessionConfigBase? other) GitHubToken = other.GitHubToken; GitHubTokenProvider = other.GitHubTokenProvider; RemoteSession = other.RemoteSession; + FeatureFlags = other.FeatureFlags is not null + ? new Dictionary(other.FeatureFlags) + : null; ExpAssignments = other.ExpAssignments; EnableManagedSettings = other.EnableManagedSettings; ManagedSettings = other.ManagedSettings; @@ -3725,6 +3728,12 @@ protected SessionConfigBase(SessionConfigBase? other) [EditorBrowsable(EditorBrowsableState.Never)] public CopilotExpAssignmentResponse? ExpAssignments { get; set; } + ///

+ /// Feature-flag values resolved by the host for this session. + /// Re-supply them when resuming after a runtime restart. + /// + public IDictionary? FeatureFlags { get; set; } + /// /// Opt-in: when true, the runtime self-fetches enterprise managed /// settings (bypass-permissions policy) at session bootstrap using the diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 6edf168093..2414093797 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -630,6 +630,46 @@ public void ResumeSessionConfigClone_PreservesExpAssignments() Assert.Equal("exp-resume", clone.ExpAssignments!.Configs[0].Id); } + [Fact] + public void SessionRequests_CanSerializeFeatureFlags_WithSdkOptions() + { + var options = GetSerializerOptions(); + var flags = new Dictionary + { + ["ENABLED_TEST_FLAG"] = true, + ["DISABLED_TEST_FLAG"] = false, + }; + + foreach (var requestName in new[] { "CreateSessionRequest", "ResumeSessionRequest" }) + { + var requestType = GetNestedType(typeof(CopilotClient), requestName); + var request = CreateInternalRequest( + requestType, + ("SessionId", "session-id"), + ("FeatureFlags", flags)); + using var document = JsonDocument.Parse( + JsonSerializer.Serialize(request, requestType, options)); + var serializedFlags = document.RootElement.GetProperty("featureFlags"); + Assert.True(serializedFlags.GetProperty("ENABLED_TEST_FLAG").GetBoolean()); + Assert.False(serializedFlags.GetProperty("DISABLED_TEST_FLAG").GetBoolean()); + } + } + + [Fact] + public void SessionConfigClone_CopiesFeatureFlags() + { + var config = new SessionConfig + { + FeatureFlags = new Dictionary { ["TEST_FLAG"] = true }, + }; + + var clone = config.Clone(); + clone.FeatureFlags!["TEST_FLAG"] = false; + + Assert.True(config.FeatureFlags["TEST_FLAG"]); + Assert.False(clone.FeatureFlags["TEST_FLAG"]); + } + [Fact] public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions() { diff --git a/go/client.go b/go/client.go index 1b45a9f142..8f6b41a983 100644 --- a/go/client.go +++ b/go/client.go @@ -906,6 +906,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.ExtensionSDKPath = config.ExtensionSDKPath req.ExtensionInfo = config.ExtensionInfo req.ExpAssignments = config.ExpAssignments + if config.FeatureFlags != nil { + req.FeatureFlags = &config.FeatureFlags + } req.EnableManagedSettings = config.EnableManagedSettings req.ManagedSettings = config.ManagedSettings @@ -1316,6 +1319,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.ExtensionSDKPath = config.ExtensionSDKPath req.ExtensionInfo = config.ExtensionInfo req.ExpAssignments = config.ExpAssignments + if config.FeatureFlags != nil { + req.FeatureFlags = &config.FeatureFlags + } req.EnableManagedSettings = config.EnableManagedSettings req.ManagedSettings = config.ManagedSettings if config.OnPermissionRequest != nil { diff --git a/go/client_test.go b/go/client_test.go index d49aff1cc1..ec3924f0a9 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -4012,6 +4012,62 @@ func TestResumeSessionRequest_ExpAssignments(t *testing.T) { }) } +func TestSessionRequests_FeatureFlags(t *testing.T) { + featureFlags := map[string]bool{ + "ENABLED_TEST_FLAG": true, + "DISABLED_TEST_FLAG": false, + } + + for _, tc := range []struct { + name string + req any + }{ + {"create", createSessionRequest{FeatureFlags: &featureFlags}}, + {"resume", resumeSessionRequest{SessionID: "s1", FeatureFlags: &featureFlags}}, + } { + t.Run(tc.name, func(t *testing.T) { + data, err := json.Marshal(tc.req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(data, &payload); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + got := payload["featureFlags"].(map[string]any) + if got["ENABLED_TEST_FLAG"] != true || got["DISABLED_TEST_FLAG"] != false { + t.Errorf("Unexpected featureFlags: %v", got) + } + }) + } + + emptyFlags := map[string]bool{} + for _, tc := range []struct { + name string + req any + }{ + {"create empty", createSessionRequest{FeatureFlags: &emptyFlags}}, + {"resume empty", resumeSessionRequest{SessionID: "s1", FeatureFlags: &emptyFlags}}, + {"create unset", createSessionRequest{}}, + {"resume unset", resumeSessionRequest{SessionID: "s1"}}, + } { + t.Run(tc.name, func(t *testing.T) { + data, err := json.Marshal(tc.req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(data, &payload); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + _, present := payload["featureFlags"] + if strings.Contains(tc.name, "empty") != present { + t.Errorf("featureFlags presence = %v for %s", present, tc.name) + } + }) + } +} + func TestIsTerminal(t *testing.T) { t.Run("IsTerminal is serialized in tool definition", func(t *testing.T) { tool := Tool{ diff --git a/go/types.go b/go/types.go index a574168aba..3577731187 100644 --- a/go/types.go +++ b/go/types.go @@ -1574,6 +1574,9 @@ type SessionConfig struct { // intended for trusted out-of-process integrators, and is not intended for // general external use. ExpAssignments *CopilotExpAssignmentResponse + // FeatureFlags contains feature-flag values resolved by the host for this session. + // Re-supply them when resuming after a runtime restart. + FeatureFlags map[string]bool // EnableManagedSettings, when set to true, opts the runtime into // self-fetching enterprise managed settings (bypass-permissions policy) at // session bootstrap using the session's GitHubToken. Requires GitHubToken to @@ -2105,6 +2108,9 @@ type ResumeSessionConfig struct { // intended for trusted out-of-process integrators, and is not intended for // general external use. ExpAssignments *CopilotExpAssignmentResponse + // FeatureFlags contains host-resolved feature-flag values to apply on resume. + // See SessionConfig.FeatureFlags. + FeatureFlags map[string]bool // EnableManagedSettings injects the same opt-in flag on resume. See // SessionConfig.EnableManagedSettings. Re-supply on resume so the runtime // re-applies the managed-settings self-fetch after a CLI process restart. @@ -2578,6 +2584,7 @@ type createSessionRequest struct { ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"` ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"` + FeatureFlags *map[string]bool `json:"featureFlags,omitempty"` EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` @@ -2678,6 +2685,7 @@ type resumeSessionRequest struct { ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"` ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"` + FeatureFlags *map[string]bool `json:"featureFlags,omitempty"` EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index e0a8655390..d54889cc4b 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -201,6 +201,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setGitHubToken(config.getGitHubToken()); request.setRemoteSession(config.getRemoteSession()); request.setCloud(config.getCloud()); + request.setFeatureFlags(config.getFeatureFlags()); request.setExpAssignments(config.getExpAssignments()); config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); request.setManagedSettings(config.getManagedSettings()); @@ -340,6 +341,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo } request.setGitHubToken(config.getGitHubToken()); request.setRemoteSession(config.getRemoteSession()); + request.setFeatureFlags(config.getFeatureFlags()); request.setExpAssignments(config.getExpAssignments()); config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); request.setManagedSettings(config.getManagedSettings()); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index ef3531fa5f..f6b5001e7a 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -239,6 +239,9 @@ public final class CreateSessionRequest { @JsonProperty("expAssignments") private CopilotExpAssignmentResponse expAssignments; + @JsonProperty("featureFlags") + private Map featureFlags; + @JsonProperty("enableManagedSettings") @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableManagedSettings; @@ -1126,6 +1129,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) { this.expAssignments = expAssignments; } + /** Gets host-resolved feature flags. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** Sets host-resolved feature flags. @param featureFlags the feature flags */ + public void setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + } + /** * Gets the self-fetch managed settings flag. @return the flag, or {@code null} * if not set diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index d13b67d715..ac4f71e07b 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -112,6 +112,7 @@ public class ResumeSessionConfig { private String remoteSession; private CopilotExpAssignmentResponse expAssignments; private Boolean enableManagedSettings; + private Map featureFlags; private ManagedSettings managedSettings; /** @@ -1995,6 +1996,23 @@ public ResumeSessionConfig setExpAssignments(CopilotExpAssignmentResponse expAss return this; } + /** Gets host-resolved feature-flag values. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** + * Sets feature-flag values resolved by the host to apply on resume. + * + * @param featureFlags + * the feature flags + * @return this config for method chaining + */ + public ResumeSessionConfig setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + return this; + } + /** * Gets whether the runtime self-fetches enterprise managed settings at session * bootstrap on resume. @@ -2129,6 +2147,7 @@ public ResumeSessionConfig clone() { copy.gitHubToken = this.gitHubToken; copy.gitHubTokenProvider = this.gitHubTokenProvider; copy.remoteSession = this.remoteSession; + copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null; copy.expAssignments = this.expAssignments; copy.enableManagedSettings = this.enableManagedSettings; copy.managedSettings = this.managedSettings; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 99e4090438..776d58137b 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -241,6 +241,9 @@ public final class ResumeSessionRequest { @JsonProperty("expAssignments") private CopilotExpAssignmentResponse expAssignments; + @JsonProperty("featureFlags") + private Map featureFlags; + @JsonProperty("enableManagedSettings") @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableManagedSettings; @@ -1141,6 +1144,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) { this.expAssignments = expAssignments; } + /** Gets host-resolved feature flags. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** Sets host-resolved feature flags. @param featureFlags the feature flags */ + public void setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + } + /** * Gets the self-fetch managed settings flag. @return the flag, or {@code null} * if not set diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index cc2a858f68..cbcacd9771 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -113,6 +113,7 @@ public class SessionConfig { private CloudSessionOptions cloud; private CopilotExpAssignmentResponse expAssignments; private Boolean enableManagedSettings; + private Map featureFlags; private ManagedSettings managedSettings; /** @@ -2124,6 +2125,23 @@ public SessionConfig setExpAssignments(CopilotExpAssignmentResponse expAssignmen return this; } + /** Gets host-resolved feature-flag values. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** + * Sets feature-flag values resolved by the host for this session. + * + * @param featureFlags + * the feature flags + * @return this config instance for method chaining + */ + public SessionConfig setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + return this; + } + /** * Gets whether the runtime self-fetches enterprise managed settings at session * bootstrap. @@ -2271,6 +2289,7 @@ public SessionConfig clone() { copy.gitHubTokenProvider = this.gitHubTokenProvider; copy.remoteSession = this.remoteSession; copy.cloud = this.cloud; + copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null; copy.expAssignments = this.expAssignments; copy.enableManagedSettings = this.enableManagedSettings; copy.managedSettings = this.managedSettings; diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 5ea8c69274..edc40175d0 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -1092,6 +1092,26 @@ void testBuildRequestsOmitExpAssignmentsWhenUnset() throws Exception { assertFalse(resumeJson.contains("\"expAssignments\""), "expAssignments should be omitted when null"); } + @Test + void testBuildRequestsPropagateAndSerializeFeatureFlags() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var flags = Map.of("ENABLED_TEST_FLAG", true, "DISABLED_TEST_FLAG", false); + + var createConfig = new SessionConfig().setFeatureFlags(flags); + CreateSessionRequest createRequest = SessionRequestBuilder.buildCreateRequest(createConfig, "session-1"); + assertEquals(flags, createRequest.getFeatureFlags()); + var createJson = mapper.readTree(mapper.writeValueAsString(createRequest)); + assertTrue(createJson.path("featureFlags").path("ENABLED_TEST_FLAG").asBoolean()); + assertFalse(createJson.path("featureFlags").path("DISABLED_TEST_FLAG").asBoolean()); + + var resumeConfig = new ResumeSessionConfig().setFeatureFlags(flags); + ResumeSessionRequest resumeRequest = SessionRequestBuilder.buildResumeRequest("session-1", resumeConfig); + assertEquals(flags, resumeRequest.getFeatureFlags()); + var resumeJson = mapper.readTree(mapper.writeValueAsString(resumeRequest)); + assertTrue(resumeJson.path("featureFlags").path("ENABLED_TEST_FLAG").asBoolean()); + assertFalse(resumeJson.path("featureFlags").path("DISABLED_TEST_FLAG").asBoolean()); + } + @Test void testClonePreservesAndForwardsExpAssignments() throws Exception { var mapper = JsonRpcClient.getObjectMapper(); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 4f460e9577..acaf67ba85 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1749,6 +1749,7 @@ export class CopilotClient { gitHubTokenProviderRegistrationId, remoteSession: config.remoteSession, cloud: config.cloud, + featureFlags: config.featureFlags, expAssignments: config.expAssignments, enableManagedSettings: config.enableManagedSettings, managedSettings: config.managedSettings, @@ -2020,6 +2021,7 @@ export class CopilotClient { gitHubTokenProviderRegistrationId, remoteSession: config.remoteSession, openCanvases: config.openCanvases, + featureFlags: config.featureFlags, expAssignments: config.expAssignments, enableManagedSettings: config.enableManagedSettings, managedSettings: config.managedSettings, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index e6f90490ed..85c77f8340 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -2886,6 +2886,12 @@ export interface SessionConfigBase { */ createSessionFsProvider?: (session: CopilotSession) => SessionFsProvider; + /** + * Feature-flag values resolved by the host for this session. + * Re-supply them when resuming after a runtime restart. + */ + featureFlags?: Record; + /** * ExP assignment ("flight") data injected by a trusted integrator, in the * same JSON shape the Copilot CLI fetches from the experimentation service diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 4017d00637..a08e4a6e5a 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1331,6 +1331,39 @@ describe("CopilotClient", () => { expect(resumePayload.expAssignments).toBeUndefined(); }); + it("forwards featureFlags in session.create and session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + const featureFlags = { ENABLED_TEST_FLAG: true, DISABLED_TEST_FLAG: false }; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + featureFlags, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + featureFlags, + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(createPayload.featureFlags).toEqual(featureFlags); + expect(resumePayload.featureFlags).toEqual(featureFlags); + }); + it("forwards capi options in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); diff --git a/python/copilot/client.py b/python/copilot/client.py index 1c0691b612..e50e5fa307 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -2250,6 +2250,7 @@ async def create_session( extension_info: ExtensionInfo | None = None, canvas_provider: CanvasProviderIdentity | None = None, canvas_handler: CanvasHandler | None = None, + feature_flags: dict[str, bool] | None = None, exp_assignments: CopilotExpAssignmentResponse | None = None, enable_managed_settings: bool | None = None, github_mcp_tool_config: GitHubMcpToolConfig | None = None, @@ -2393,6 +2394,9 @@ async def create_session( on its own and has no effect unless MCP Apps are enabled for the session (see ``enable_mcp_apps``). Omitted from the wire payload entirely when None. + feature_flags: Feature-flag values resolved by the host for this + session. Re-supply them when resuming after a runtime restart. + Sent on the wire as ``featureFlags``. exp_assignments: ExP assignment ("flight") data injected by a trusted integrator, in the same JSON shape the Copilot CLI fetches from the experimentation service @@ -2571,6 +2575,9 @@ async def create_session( if cloud is not None: payload["cloud"] = _cloud_session_options_to_dict(cloud) + if feature_flags is not None: + payload["featureFlags"] = feature_flags + # Add ExP assignment data if provided (trusted integrator) if exp_assignments is not None: payload["expAssignments"] = _exp_assignment_response_to_dict(exp_assignments) @@ -3021,6 +3028,7 @@ async def resume_session( canvas_provider: CanvasProviderIdentity | None = None, canvas_handler: CanvasHandler | None = None, open_canvases: list[OpenCanvasInstance] | None = None, + feature_flags: dict[str, bool] | None = None, exp_assignments: CopilotExpAssignmentResponse | None = None, enable_managed_settings: bool | None = None, github_mcp_tool_config: GitHubMcpToolConfig | None = None, @@ -3166,6 +3174,8 @@ async def resume_session( tool calls or permission prompts that were still pending when the session was last suspended. When False (the default), the runtime treats pending work as interrupted on resume. + feature_flags: Feature-flag values resolved by the host to apply + on resume. Sent on the wire as ``featureFlags``. exp_assignments: ExP assignment ("flight") data injected by a trusted integrator, in the same JSON shape the Copilot CLI fetches from the experimentation service @@ -3364,6 +3374,9 @@ async def resume_session( if remote_session is not None: payload["remoteSession"] = remote_session.value + if feature_flags is not None: + payload["featureFlags"] = feature_flags + # Add ExP assignment data if provided (trusted integrator) if exp_assignments is not None: payload["expAssignments"] = _exp_assignment_response_to_dict(exp_assignments) diff --git a/python/test_client.py b/python/test_client.py index cb78db2094..e475dba0e2 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -1412,6 +1412,41 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_feature_flags(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + feature_flags = {"ENABLED_TEST_FLAG": True, "DISABLED_TEST_FLAG": False} + + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + feature_flags=feature_flags, + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + feature_flags=feature_flags, + ) + + assert captured["session.create"]["featureFlags"] == feature_flags + assert captured["session.resume"]["featureFlags"] == feature_flags + finally: + await client.force_stop() + class TestURLParsing: def test_parse_port_only_url(self): diff --git a/rust/src/types.rs b/rust/src/types.rs index 726bc48f4c..89906f9627 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -2158,6 +2158,12 @@ pub struct SessionConfig { /// each command appears as `/name` for the user to invoke and the /// associated [`CommandHandler`] is called when executed. pub commands: Option>, + /// Feature-flag values resolved by the host for this session. + /// + /// Re-supply these values through [`ResumeSessionConfig::feature_flags`] + /// when resuming after a CLI process restart. Set via + /// [`with_feature_flags`](Self::with_feature_flags). + pub feature_flags: Option>, /// ExP assignment ("flight") data injected by a trusted integrator, in /// the same JSON shape the Copilot CLI fetches from the experimentation /// service (`CopilotExpAssignmentResponse`). When supplied, the runtime @@ -2336,6 +2342,7 @@ impl std::fmt::Debug for SessionConfig { &self.include_sub_agent_streaming_events, ) .field("commands", &self.commands) + .field("feature_flags", &self.feature_flags) .field("exp_assignments", &self.exp_assignments) .field("enable_managed_settings", &self.enable_managed_settings) .field("enable_experimental_mode", &self.enable_experimental_mode) @@ -2453,6 +2460,7 @@ impl Default for SessionConfig { cloud: None, include_sub_agent_streaming_events: None, commands: None, + feature_flags: None, exp_assignments: None, enable_managed_settings: None, managed_settings: None, @@ -2628,6 +2636,7 @@ impl SessionConfig { include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, enable_github_telemetry_forwarding: None, commands: wire_commands, + feature_flags: self.feature_flags, exp_assignments: self.exp_assignments, enable_managed_settings: self.enable_managed_settings, is_experimental_mode: self.enable_experimental_mode, @@ -3267,6 +3276,12 @@ impl SessionConfig { self } + /// Set feature-flag values resolved by the host for this session. + pub fn with_feature_flags(mut self, feature_flags: HashMap) -> Self { + self.feature_flags = Some(feature_flags); + self + } + /// Inject ExP assignment ("flight") data for this session, in the same /// JSON shape the Copilot CLI fetches from the experimentation service /// (`CopilotExpAssignmentResponse`). The runtime feeds it into the same @@ -3495,6 +3510,10 @@ pub struct ResumeSessionConfig { /// [`SessionConfig::commands`] — commands are not persisted server-side, /// so the resume payload re-supplies the registration. pub commands: Option>, + /// Feature-flag values resolved by the host to apply on resume. + /// + /// See [`SessionConfig::feature_flags`]. + pub feature_flags: Option>, /// ExP assignment ("flight") data injected on resume. See /// [`SessionConfig::exp_assignments`]. Re-supply on resume so the runtime /// re-applies the assignments after a CLI process restart. Set via @@ -3659,6 +3678,7 @@ impl std::fmt::Debug for ResumeSessionConfig { &self.include_sub_agent_streaming_events, ) .field("commands", &self.commands) + .field("feature_flags", &self.feature_flags) .field("exp_assignments", &self.exp_assignments) .field("enable_managed_settings", &self.enable_managed_settings) .field("enable_experimental_mode", &self.enable_experimental_mode) @@ -3826,6 +3846,7 @@ impl ResumeSessionConfig { include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, enable_github_telemetry_forwarding: None, commands: wire_commands, + feature_flags: self.feature_flags, exp_assignments: self.exp_assignments, enable_managed_settings: self.enable_managed_settings, is_experimental_mode: self.enable_experimental_mode, @@ -3926,6 +3947,7 @@ impl ResumeSessionConfig { remote_session: None, include_sub_agent_streaming_events: None, commands: None, + feature_flags: None, exp_assignments: None, enable_managed_settings: None, managed_settings: None, @@ -4551,6 +4573,12 @@ impl ResumeSessionConfig { self } + /// Re-supply feature-flag values resolved by the host on resume. + pub fn with_feature_flags(mut self, feature_flags: HashMap) -> Self { + self.feature_flags = Some(feature_flags); + self + } + /// Inject ExP assignment ("flight") data on resume. See /// [`SessionConfig::with_exp_assignments`]. Re-supply the assignments on /// resume so the runtime re-applies them after a CLI process restart. @@ -6451,6 +6479,46 @@ mod tests { assert!(empty_json.get("memory").is_none()); } + #[test] + fn feature_flags_serialize_on_create_and_resume() { + let feature_flags = HashMap::from([ + ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true), + ("DISABLED_TEST_FLAG".to_string(), false), + ]); + let expected = serde_json::json!({ + "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true, + "DISABLED_TEST_FLAG": false, + }); + + let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone()); + assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags)); + let (create_wire, _) = create_config + .into_wire(Some(SessionId::from("feature-flags-create"))) + .expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["featureFlags"], expected); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume")) + .with_feature_flags(feature_flags) + .into_wire() + .expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["featureFlags"], expected); + + let (unset_create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("feature-flags-create-unset"))) + .expect("no duplicate handlers"); + let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); + assert!(unset_create_json.get("featureFlags").is_none()); + + let (unset_resume_wire, _) = + ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); + assert!(unset_resume_json.get("featureFlags").is_none()); + } + fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse { CopilotExpAssignmentResponse { features: vec!["copilot_exp_flag".to_string()], diff --git a/rust/src/wire.rs b/rust/src/wire.rs index ebc050477e..75e17f4e9c 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -13,6 +13,7 @@ //! configs hold trait-object handlers, the wire structs hold only the //! plain data the runtime needs. +use std::collections::HashMap; use std::path::PathBuf; use indexmap::IndexMap; @@ -191,6 +192,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub commands: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub feature_flags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_managed_settings: Option, @@ -351,6 +354,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub continue_pending_work: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub feature_flags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_managed_settings: Option, From 913cb61302d6e27789c51dcacccfb193569eea73 Mon Sep 17 00:00:00 2001 From: examon <1676292+examon@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:54:02 +0000 Subject: [PATCH 17/60] docs: remove false 30-minute idle timeout claim from deployment guides (#2417) The Limitations tables in docs/setup/scaling.md and docs/setup/backend-services.md state that the CLI automatically cleans up sessions after 30 minutes of inactivity. There is no such timeout: idle sessions are cleaned up only when a positive --session-idle-timeout is configured on the server, and nothing is configured by default. Replace both rows with the opt-in framing already used by docs/features/session-persistence.md. Both guides describe connecting to a separately launched CLI server, where the SDK-side sessionIdleTimeoutSeconds option is documented as ignored, so the rows point at the --session-idle-timeout server flag instead. Co-authored-by: examon --- docs/setup/backend-services.md | 2 +- docs/setup/scaling.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index 7f1da36e82..b7b288bb8f 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -540,7 +540,7 @@ setInterval(() => cleanupSessions(24 * 60 * 60 * 1000), 60 * 60 * 1000); | **Single CLI server = single point of failure** | See [Scaling guide](./scaling.md) for HA patterns | | **No built-in auth between SDK and CLI** | Secure the network path (same host, VPC, etc.) | | **Session state on local disk** | Mount persistent storage for container restarts | -| **30-minute idle timeout** | Sessions without activity are auto-cleaned | +| **No idle timeout by default** | Pass `--session-idle-timeout ` to the CLI server to automatically clean up inactive sessions | ## When to move on diff --git a/docs/setup/scaling.md b/docs/setup/scaling.md index c4a7a0953f..acf82bdb26 100644 --- a/docs/setup/scaling.md +++ b/docs/setup/scaling.md @@ -626,7 +626,7 @@ flowchart TB | **No built-in session locking** | Implement application-level locking for concurrent access | | **No built-in load balancing** | Use external LB or service mesh | | **Session state is file-based** | Requires shared filesystem for multi-server setups | -| **30-minute idle timeout** | Sessions without activity are auto-cleaned by the CLI | +| **No idle timeout by default** | Pass `--session-idle-timeout ` to the CLI server to automatically clean up inactive sessions | | **CLI is single-process** | Scale by adding more CLI server instances, not threads | ## Next steps From 6b0252cf0e8243d5c972d4dea00c1d8862b57d9f Mon Sep 17 00:00:00 2001 From: Quim Muntal Date: Tue, 1 Sep 2026 18:05:14 +0000 Subject: [PATCH 18/60] [go] Avoid duplicate bundler license downloads (#2453) --- go/cmd/bundler/main.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index e7bbd7a64e..89f99daf1b 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -124,6 +124,10 @@ func main() { os.Exit(1) } } + if err := downloadCLILicense(version, outputPath); err != nil { + fmt.Fprintf(os.Stderr, "Error: failed to download CLI license: %v\n", err) + os.Exit(1) + } // Generate the Go file with embed directive if err := generateGoFile( @@ -384,9 +388,6 @@ func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (bundle if err != nil { return bundleArtifacts{}, fmt.Errorf("failed to hash existing output: %w", err) } - if err := downloadCLILicense(cliVersion, outputPath); err != nil { - return bundleArtifacts{}, fmt.Errorf("failed to download CLI license: %w", err) - } runtimeHash, err := sha256FileFromCompressed(runtimeArtifactPath) if err != nil { return bundleArtifacts{}, fmt.Errorf("failed to hash existing runtime.node: %w", err) @@ -427,9 +428,6 @@ func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (bundle if err := compressZstdFile(binaryPath, outputPath); err != nil { return bundleArtifacts{}, fmt.Errorf("failed to write output binary: %w", err) } - if err := downloadCLILicense(cliVersion, outputPath); err != nil { - return bundleArtifacts{}, fmt.Errorf("failed to download CLI license: %w", err) - } rawLibPath := filepath.Join(tempDir, "runtime.node") if err := extractFileFromTarball( From 76f6b6daf7816441d44312f128755d327fbeff5e Mon Sep 17 00:00:00 2001 From: Alberto Gimeno Date: Tue, 1 Sep 2026 18:54:52 +0000 Subject: [PATCH 19/60] Make Rust CLI ownership crash-safe on Windows (#2458) * Fix CLI process tree lifecycle Contain SDK-spawned CLI descendants in Unix process groups and Windows Job Objects, and terminate the complete owned tree during shutdown and failure cleanup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Disarm process tree before root reap Consume process-tree ownership during termination to prevent a second Unix signal after PID reuse, and make the Windows failed-start fixture execute through a PowerShell script. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Add real-CLI E2E coverage for descendant and host-crash scenarios - Add stop_terminates_real_cli_wrapper_descendants: a Unix E2E test that launches the real bundled CLI through a shell wrapper (via ClientOptions::prefix_args) that backgrounds a descendant process, and verifies Client::stop() kills that inherited descendant. - Add abrupt_host_termination_still_kills_cli_via_job_object: a Windows E2E test representing the actual github/app#2303 scenario -- an SDK-embedding host process being killed/crashing abruptly, so none of its own cleanup code (Client::stop/force_stop/Drop) ever runs. It spawns a new copilot-host-crash-fixture helper binary that starts a real CLI client and then never calls any SDK teardown code, terminates that helper abruptly, and asserts the CLI still dies via the Job Object's kill-on-close semantics. Manually reproducing the same abrupt-kill scenario on Linux showed the CLI already exits on its own within ~200ms once the OS closes the dead host's end of the stdio pipe, with none of this crate's code involved -- a pre-existing, code-free safety net specific to stdio-piped processes on Unix. That's further evidence Unix process-group containment isn't needed to address #2303's failure mode, which is Windows-only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(rust): narrow process ownership to Windows crash safety Use a kill-on-close Job Object only on Windows and test that abruptly terminating the SDK host tears down its owned CLI process. Preserve direct-child behavior on other platforms. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test(rust): focus process ownership coverage on Windows Keep the real-CLI abrupt-host E2E and remove unsupported Unix descendant coverage and the redundant synthetic crash test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson --- rust/Cargo.lock | 1 + rust/Cargo.toml | 13 ++ rust/src/lib.rs | 80 ++++++--- rust/src/process_tree.rs | 195 ++++++++++++++++++++++ rust/tests/e2e/client_lifecycle.rs | 147 ++++++++++++++++ rust/tests/fixtures/host_crash_fixture.rs | 60 +++++++ 6 files changed, 471 insertions(+), 25 deletions(-) create mode 100644 rust/src/process_tree.rs create mode 100644 rust/tests/fixtures/host_crash_fixture.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8de6797989..b91eebd06c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -454,6 +454,7 @@ dependencies = [ "tracing", "ureq", "uuid", + "windows-sys 0.61.2", "zip", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index a28190cd1f..c7a704fd50 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -70,6 +70,12 @@ tokio-tungstenite = { version = "0.24", default-features = false, features = ["c [target.'cfg(windows)'.dependencies] zip = { version = "2", default-features = false, features = ["deflate"], optional = true } +windows-sys = { version = "0.61", default-features = false, features = [ + "Win32_Foundation", + "Win32_System_Diagnostics_ToolHelp", + "Win32_System_JobObjects", + "Win32_System_Threading", +] } [dev-dependencies] rusqlite = { version = "0.35", features = ["bundled"] } @@ -105,6 +111,13 @@ required-features = ["test-support"] test = false bench = false +[[bin]] +name = "copilot-host-crash-fixture" +path = "tests/fixtures/host_crash_fixture.rs" +required-features = ["test-support"] +test = false +bench = false + [build-dependencies] base64 = "0.22" dirs = "5" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 1c20fd1837..32d44ddc0f 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -33,6 +33,7 @@ pub mod hooks; mod jsonrpc; /// Permission-policy helpers that produce a [`handler::PermissionHandler`]. pub mod permission; +mod process_tree; /// BYOK bearer-token provider callbacks. pub mod provider_token; mod provider_token_dispatch; @@ -1066,6 +1067,7 @@ impl std::fmt::Debug for Client { struct ClientInner { child: parking_lot::Mutex>, + process_tree: parking_lot::Mutex>, #[cfg(feature = "bundled-in-process")] /// In-process FFI runtime host, set only for [`Transport::InProcess`]. /// Closing it tears down the native runtime connection. @@ -1302,6 +1304,7 @@ impl Client { reader, writer, None, + None, working_directory, options.on_list_models, extension_launch_provider.clone(), @@ -1317,7 +1320,7 @@ impl Client { port, connection_token: _, } => { - let (mut child, actual_port, spawn_elapsed, port_wait_elapsed) = + let (mut child, tree, actual_port, spawn_elapsed, port_wait_elapsed) = Self::spawn_tcp(&program, &options, &working_directory, port).await?; timings.process_spawn_ms = Some(StartupTimings::millis(spawn_elapsed)); timings.port_wait_ms = Some(StartupTimings::millis(port_wait_elapsed)); @@ -1334,6 +1337,7 @@ impl Client { reader, writer, Some(child), + tree, working_directory, options.on_list_models, extension_launch_provider.clone(), @@ -1346,7 +1350,7 @@ impl Client { )? } Transport::Stdio => { - let (mut child, spawn_elapsed) = + let (mut child, tree, spawn_elapsed) = Self::spawn_stdio(&program, &options, &working_directory)?; timings.process_spawn_ms = Some(StartupTimings::millis(spawn_elapsed)); let stdin = child.stdin.take().expect("stdin is piped"); @@ -1356,6 +1360,7 @@ impl Client { stdout, stdin, Some(child), + tree, working_directory, options.on_list_models, extension_launch_provider.clone(), @@ -1422,6 +1427,7 @@ impl Client { reader, writer, None, + None, working_directory, options.on_list_models, extension_launch_provider.clone(), @@ -1563,6 +1569,7 @@ impl Client { reader, writer, None, + None, cwd, None, None, @@ -1589,6 +1596,7 @@ impl Client { reader, writer, None, + None, cwd, None, Some(provider), @@ -1619,6 +1627,7 @@ impl Client { reader, writer, None, + None, cwd, None, None, @@ -1645,6 +1654,7 @@ impl Client { reader, writer, None, + None, cwd, None, None, @@ -1671,6 +1681,7 @@ impl Client { reader, writer, None, + None, cwd, None, None, @@ -1698,6 +1709,7 @@ impl Client { reader: impl AsyncRead + Unpin + Send + 'static, writer: impl AsyncWrite + Unpin + Send + 'static, child: Option, + process_tree: Option, cwd: PathBuf, on_list_models: Option>, extension_launch_provider: Option< @@ -1732,6 +1744,7 @@ impl Client { let client = Self { inner: Arc::new(ClientInner { child: parking_lot::Mutex::new(child), + process_tree: parking_lot::Mutex::new(process_tree), #[cfg(feature = "bundled-in-process")] ffi_host: parking_lot::Mutex::new(None), rpc, @@ -1870,13 +1883,6 @@ impl Client { .stdout(Stdio::piped()) .stderr(Stdio::piped()); - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - command.as_std_mut().creation_flags(CREATE_NO_WINDOW); - } - command } @@ -1933,7 +1939,7 @@ impl Client { program: &Path, options: &ClientOptions, working_directory: &Path, - ) -> Result<(Child, Duration)> { + ) -> Result<(Child, Option, Duration)> { info!(cwd = ?working_directory, program = %program.display(), "spawning copilot CLI (stdio)"); let mut command = Self::build_command(program, options, working_directory); command @@ -1945,13 +1951,13 @@ impl Client { .args(&options.extra_args) .stdin(Stdio::piped()); let spawn_start = Instant::now(); - let child = command.spawn()?; + let (child, tree) = process_tree::spawn(&mut command)?; let spawn_elapsed = spawn_start.elapsed(); debug!( elapsed_ms = spawn_elapsed.as_millis(), "Client::spawn_stdio subprocess spawned" ); - Ok((child, spawn_elapsed)) + Ok((child, tree, spawn_elapsed)) } async fn spawn_tcp( @@ -1959,7 +1965,13 @@ impl Client { options: &ClientOptions, working_directory: &Path, port: u16, - ) -> Result<(Child, u16, Duration, Duration)> { + ) -> Result<( + Child, + Option, + u16, + Duration, + Duration, + )> { info!(cwd = ?working_directory, program = %program.display(), port = %port, "spawning copilot CLI (tcp)"); let mut command = Self::build_command(program, options, working_directory); command @@ -1971,7 +1983,7 @@ impl Client { .args(&options.extra_args) .stdin(Stdio::null()); let spawn_start = Instant::now(); - let mut child = command.spawn()?; + let (mut child, tree) = process_tree::spawn(&mut command)?; let spawn_elapsed = spawn_start.elapsed(); debug!( elapsed_ms = spawn_elapsed.as_millis(), @@ -2018,7 +2030,7 @@ impl Client { "Client::spawn_tcp TCP port wait complete" ); info!(port = %actual_port, "CLI server listening"); - Ok((child, actual_port, spawn_elapsed, port_wait_elapsed)) + Ok((child, tree, actual_port, spawn_elapsed, port_wait_elapsed)) } fn drain_stderr(child: &mut Child) { @@ -2561,11 +2573,12 @@ impl Client { /// Cooperatively shut down the client and the CLI child process. /// /// Walks every still-registered session and sends `session.destroy` - /// for each one, asks SDK-owned runtimes to shut down, then kills the - /// CLI child. Errors from per-session destroys, runtime shutdown, and - /// the final child-kill are collected into - /// [`StopErrors`] rather than short-circuiting on the first failure - /// — so callers see the full picture of teardown. + /// for each one, asks SDK-owned runtimes to shut down, terminates the + /// Windows-owned CLI Job Object when present, and reaps the root process. + /// Errors from per-session destroys, runtime shutdown, and final process + /// termination are collected into [`StopErrors`] rather than + /// short-circuiting on the first failure — so callers see the full picture + /// of teardown. /// /// If you have already called [`Session::disconnect`] on every /// session this client created, the per-session destroy step is a @@ -2654,8 +2667,14 @@ impl Client { } let child = self.inner.child.lock().take(); + let process_tree = self.inner.process_tree.lock().take(); *self.inner.state.lock() = ConnectionState::Disconnected; *self.inner.models_cache.lock() = Arc::new(tokio::sync::OnceCell::new()); + if let Some(process_tree) = process_tree + && let Err(error) = process_tree.terminate() + { + errors.push(error.into()); + } if let Some(mut child) = child { match child.try_wait() { Ok(Some(_status)) => {} @@ -2696,10 +2715,9 @@ impl Client { /// /// Synchronous fallback when [`stop`](Self::stop) is unsuitable — for /// example when the awaiting tokio runtime is shutting down or the - /// process is wedged on I/O. Sends a kill signal without awaiting - /// reaper completion and immediately drops all per-session router - /// state so dependent tasks observe a closed channel rather than a - /// hang. + /// process is wedged on I/O. Terminates the Windows-owned CLI Job Object + /// when present and immediately drops all per-session router state so + /// dependent tasks observe a closed channel rather than a hang. /// /// # Cancel safety /// @@ -2725,6 +2743,11 @@ impl Client { let pid = self.pid(); info!(pid = ?pid, "force-stopping CLI process"); self.inner.extension_launch_provider.clear(); + if let Some(process_tree) = self.inner.process_tree.lock().take() + && let Err(error) = process_tree.terminate() + { + error!(pid = ?pid, %error, "failed to terminate CLI process tree"); + } if let Some(mut child) = self.inner.child.lock().take() && let Err(e) = child.start_kill() { @@ -2786,8 +2809,13 @@ impl Client { impl Drop for ClientInner { fn drop(&mut self) { + let pid = self.child.lock().as_ref().and_then(Child::id); + if let Some(process_tree) = self.process_tree.lock().take() + && let Err(error) = process_tree.terminate() + { + error!(pid = ?pid, %error, "failed to terminate CLI process tree on drop"); + } if let Some(ref mut child) = *self.child.lock() { - let pid = child.id(); if let Err(e) = child.start_kill() { error!(pid = ?pid, error = %e, "failed to kill CLI process on drop"); } else { @@ -3447,6 +3475,7 @@ mod tests { client_read, client_write, Some(child), + None, temp.path().to_path_buf(), None, None, @@ -3536,6 +3565,7 @@ mod tests { Client { inner: Arc::new(ClientInner { child: parking_lot::Mutex::new(None), + process_tree: parking_lot::Mutex::new(None), #[cfg(feature = "bundled-in-process")] ffi_host: parking_lot::Mutex::new(None), rpc: { diff --git a/rust/src/process_tree.rs b/rust/src/process_tree.rs new file mode 100644 index 0000000000..8dc7a451c5 --- /dev/null +++ b/rust/src/process_tree.rs @@ -0,0 +1,195 @@ +//! Windows crash-safe ownership of an SDK-spawned CLI process. +//! +//! A Job Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` lets Windows +//! terminate the CLI when the SDK-hosting process exits abruptly, even when +//! Rust cleanup code never runs. Other platforms retain Tokio's direct-child +//! ownership because no equivalent product failure has been demonstrated. + +use std::io; + +use tokio::process::{Child, Command}; + +pub(crate) fn spawn(command: &mut Command) -> io::Result<(Child, Option)> { + #[cfg(windows)] + { + platform::spawn(command).map(|(child, tree)| (child, Some(ProcessTree(Some(tree))))) + } + #[cfg(not(windows))] + { + command.spawn().map(|child| (child, None)) + } +} + +pub(crate) struct ProcessTree(Option); + +impl ProcessTree { + pub(crate) fn terminate(mut self) -> io::Result<()> { + self.0.take().expect("process tree is armed").terminate() + } +} + +impl Drop for ProcessTree { + fn drop(&mut self) { + if let Some(tree) = self.0.take() { + let _ = tree.terminate(); + } + } +} + +#[cfg(not(windows))] +mod platform { + pub(super) struct Tree; + + impl Tree { + pub(super) fn terminate(&self) -> std::io::Result<()> { + unreachable!("process-tree ownership is Windows-only") + } + } +} + +#[cfg(windows)] +mod platform { + use std::mem::size_of; + use std::os::windows::process::CommandExt; + use std::{io, ptr}; + + use tokio::process::{Child, Command}; + use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; + use windows_sys::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next, + }; + use windows_sys::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, + SetInformationJobObject, TerminateJobObject, + }; + use windows_sys::Win32::System::Threading::{ + CREATE_NO_WINDOW, CREATE_SUSPENDED, OpenThread, ResumeThread, THREAD_SUSPEND_RESUME, + }; + + struct OwnedHandle(HANDLE); + + // SAFETY: Win32 handles may be used and closed from any thread. + unsafe impl Send for OwnedHandle {} + unsafe impl Sync for OwnedHandle {} + + impl Drop for OwnedHandle { + fn drop(&mut self) { + // SAFETY: this value uniquely owns a valid handle. + unsafe { + CloseHandle(self.0); + } + } + } + + pub(super) struct Tree { + job: OwnedHandle, + } + + pub(super) fn spawn(command: &mut Command) -> io::Result<(Child, Tree)> { + // The root cannot run or create descendants before Job assignment. + command + .as_std_mut() + .creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED); + let mut child = command.spawn()?; + match attach_and_resume(&child) { + Ok(tree) => Ok((child, tree)), + Err(error) => { + let _ = child.start_kill(); + Err(error) + } + } + } + + fn attach_and_resume(child: &Child) -> io::Result { + // SAFETY: null security attributes and name create a private, + // non-inheritable Job Object. + let raw_job = unsafe { CreateJobObjectW(ptr::null(), ptr::null()) }; + if raw_job.is_null() { + return Err(io::Error::last_os_error()); + } + let job = OwnedHandle(raw_job); + + let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default(); + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + // SAFETY: `limits` has the layout required by the selected info class. + if unsafe { + SetInformationJobObject( + job.0, + JobObjectExtendedLimitInformation, + ptr::from_ref(&limits).cast(), + size_of::() as u32, + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + + let process = child.raw_handle().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "CLI exited before Job Object assignment", + ) + })?; + // SAFETY: both handles are valid and the child is still suspended. + if unsafe { AssignProcessToJobObject(job.0, process.cast()) } == 0 { + return Err(io::Error::last_os_error()); + } + + resume_initial_thread(child.id().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "CLI exited before thread resume") + })?)?; + Ok(Tree { job }) + } + + fn resume_initial_thread(pid: u32) -> io::Result<()> { + // SAFETY: the returned snapshot handle is owned and closed below. + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) }; + if snapshot == INVALID_HANDLE_VALUE { + return Err(io::Error::last_os_error()); + } + let snapshot = OwnedHandle(snapshot); + let mut entry = THREADENTRY32 { + dwSize: size_of::() as u32, + ..Default::default() + }; + + // SAFETY: `entry` has the documented size and remains live throughout + // enumeration. + let mut found = unsafe { Thread32First(snapshot.0, &mut entry) } != 0; + while found { + if entry.th32OwnerProcessID == pid { + // SAFETY: the thread id came from the live system snapshot. + let raw_thread = + unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) }; + if raw_thread.is_null() { + return Err(io::Error::last_os_error()); + } + let thread = OwnedHandle(raw_thread); + // SAFETY: this is the root's suspended initial thread. + if unsafe { ResumeThread(thread.0) } == u32::MAX { + return Err(io::Error::last_os_error()); + } + return Ok(()); + } + // SAFETY: same valid snapshot and initialized entry as above. + found = unsafe { Thread32Next(snapshot.0, &mut entry) } != 0; + } + + Err(io::Error::new( + io::ErrorKind::NotFound, + "CLI initial thread was not found", + )) + } + + impl Tree { + pub(super) fn terminate(&self) -> io::Result<()> { + // SAFETY: the handle is a live Job Object owned by this value. + if unsafe { TerminateJobObject(self.job.0, 1) } != 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + } + } +} diff --git a/rust/tests/e2e/client_lifecycle.rs b/rust/tests/e2e/client_lifecycle.rs index 75646b4860..92bfa6ff6d 100644 --- a/rust/tests/e2e/client_lifecycle.rs +++ b/rust/tests/e2e/client_lifecycle.rs @@ -1,3 +1,5 @@ +#[cfg(windows)] +use github_copilot_sdk::CliProgram; use github_copilot_sdk::SessionLifecycleEventType; use serde_json::json; @@ -135,6 +137,151 @@ async fn dispose_disconnects_client_and_disposes_rpc_surface_drop() { .await; } +// This test represents github/app#2303: the SDK-hosting GitHub Copilot app +// process exits abruptly, so Client cleanup never runs. The helper starts a +// real CLI client, is terminated through `TerminateProcess`, and relies only +// on Job Object kill-on-close behavior to terminate the CLI. +#[cfg(windows)] +#[tokio::test] +async fn abrupt_host_termination_still_kills_cli_via_job_object() { + with_e2e_context( + "client_lifecycle", + "abrupt_host_termination_still_kills_cli_via_job_object", + |ctx| { + Box::pin(async move { + let options = ctx.client_options(); + let program = match &options.program { + CliProgram::Path(path) => path + .to_str() + .expect("CLI program path is valid UTF-8") + .to_owned(), + CliProgram::Resolve => { + panic!("E2E client options should resolve to an explicit CLI path") + } + }; + let prefix_args: Vec = options + .prefix_args + .iter() + .map(|arg| arg.to_str().expect("prefix arg is valid UTF-8").to_owned()) + .collect(); + let env_pairs: Vec<(String, String)> = options + .env + .iter() + .map(|(k, v)| { + ( + k.to_str().expect("env key is valid UTF-8").to_owned(), + v.to_str().expect("env value is valid UTF-8").to_owned(), + ) + }) + .collect(); + let cwd = options + .working_directory + .to_str() + .expect("cwd is valid UTF-8") + .to_owned(); + let pid_file = ctx.work_dir().join("host-crash-fixture-cli.pid"); + + let mut host = + std::process::Command::new(env!("CARGO_BIN_EXE_copilot-host-crash-fixture")) + .env("HOST_CRASH_FIXTURE_PROGRAM", &program) + .env( + "HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON", + serde_json::to_string(&prefix_args).expect("serialize prefix args"), + ) + .env("HOST_CRASH_FIXTURE_CWD", &cwd) + .env( + "HOST_CRASH_FIXTURE_ENV_JSON", + serde_json::to_string(&env_pairs).expect("serialize env pairs"), + ) + .env("HOST_CRASH_FIXTURE_PID_FILE", &pid_file) + .spawn() + .expect("spawn host-crash fixture process"); + + let cli_pid = wait_for_pid_file_windows(&pid_file).await; + assert!( + process_alive_windows(cli_pid), + "CLI should be alive before its host process is terminated" + ); + + // `Child::kill` maps to `TerminateProcess`, which runs none + // of the target process's cleanup code. + host.kill().expect("terminate host-crash fixture process"); + host.wait().expect("reap host-crash fixture process"); + + let cli_exited = wait_for_process_exit_windows(cli_pid).await; + if !cli_exited { + kill_process_windows(cli_pid); + } + assert!( + cli_exited, + "CLI survived its abruptly terminated host process; Job Object \ + kill-on-close did not terminate it" + ); + }) + }, + ) + .await; +} + +#[cfg(windows)] +async fn wait_for_pid_file_windows(path: &std::path::Path) -> u32 { + super::support::wait_for_condition("host-crash fixture CLI pid file", || async { + path.exists() + }) + .await; + std::fs::read_to_string(path) + .expect("read host-crash fixture CLI pid") + .trim() + .parse() + .expect("parse host-crash fixture CLI pid") +} + +#[cfg(windows)] +async fn wait_for_process_exit_windows(pid: u32) -> bool { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15); + while process_alive_windows(pid) { + if std::time::Instant::now() >= deadline { + return false; + } + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + } + true +} + +#[cfg(windows)] +fn process_alive_windows(pid: u32) -> bool { + use windows_sys::Win32::Foundation::{CloseHandle, WAIT_TIMEOUT}; + use windows_sys::Win32::System::Threading::{ + OpenProcess, PROCESS_SYNCHRONIZE, WaitForSingleObject, + }; + + // SAFETY: the process handle is closed before returning. + unsafe { + let process = OpenProcess(PROCESS_SYNCHRONIZE, 0, pid); + if process.is_null() { + return false; + } + let alive = WaitForSingleObject(process, 0) == WAIT_TIMEOUT; + CloseHandle(process); + alive + } +} + +#[cfg(windows)] +fn kill_process_windows(pid: u32) { + use windows_sys::Win32::Foundation::CloseHandle; + use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess}; + + // SAFETY: the pid came from this test's controlled fixture-spawned CLI. + unsafe { + let process = OpenProcess(PROCESS_TERMINATE, 0, pid); + if !process.is_null() { + TerminateProcess(process, 1); + CloseHandle(process); + } + } +} + #[tokio::test] async fn should_receive_session_updated_lifecycle_event_for_non_ephemeral_activity() { with_e2e_context( diff --git a/rust/tests/fixtures/host_crash_fixture.rs b/rust/tests/fixtures/host_crash_fixture.rs new file mode 100644 index 0000000000..c688cf3e87 --- /dev/null +++ b/rust/tests/fixtures/host_crash_fixture.rs @@ -0,0 +1,60 @@ +//! Test-only binary that hosts a single [`github_copilot_sdk::Client`] and then +//! blocks forever, so an external test can terminate *this* process abruptly +//! (simulating an SDK-embedding app process crashing) without ever running any +//! of this process's own cleanup code (`Client::stop`, `force_stop`, or +//! `Drop`). +//! +//! Configuration is passed entirely through environment variables so the +//! caller doesn't need this crate's non-`pub` types: +//! - `HOST_CRASH_FIXTURE_PROGRAM`: CLI program path. +//! - `HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON`: JSON array of prefix args. +//! - `HOST_CRASH_FIXTURE_CWD`: working directory for the spawned CLI. +//! - `HOST_CRASH_FIXTURE_ENV_JSON`: JSON array of `[key, value]` pairs to set +//! on the spawned CLI's environment. +//! - `HOST_CRASH_FIXTURE_PID_FILE`: path this process writes the CLI child's +//! OS process id to, once the client finishes starting. + +use std::path::PathBuf; + +use github_copilot_sdk::{CliProgram, Client, ClientOptions, Transport}; + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let program = std::env::var("HOST_CRASH_FIXTURE_PROGRAM").expect("HOST_CRASH_FIXTURE_PROGRAM"); + let prefix_args: Vec = serde_json::from_str( + &std::env::var("HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON") + .expect("HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON"), + ) + .expect("parse HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON"); + let cwd = std::env::var("HOST_CRASH_FIXTURE_CWD").expect("HOST_CRASH_FIXTURE_CWD"); + let env_pairs: Vec<(String, String)> = serde_json::from_str( + &std::env::var("HOST_CRASH_FIXTURE_ENV_JSON").expect("HOST_CRASH_FIXTURE_ENV_JSON"), + ) + .expect("parse HOST_CRASH_FIXTURE_ENV_JSON"); + let pid_file = PathBuf::from( + std::env::var("HOST_CRASH_FIXTURE_PID_FILE").expect("HOST_CRASH_FIXTURE_PID_FILE"), + ); + + let options = ClientOptions::new() + .with_program(CliProgram::Path(PathBuf::from(program))) + .with_prefix_args(prefix_args) + .with_cwd(PathBuf::from(cwd)) + .with_env(env_pairs) + .with_use_logged_in_user(false) + .with_transport(Transport::Stdio); + + let client = Client::start(options).await.expect("start CLI client"); + let pid = client.pid().expect("client reports spawned CLI pid"); + std::fs::write(&pid_file, pid.to_string()).expect("write pid file"); + + // Deliberately leak the client so nothing in this process — including its + // `Drop` impls — ever runs cleanup code. The external test process + // terminates this process abruptly (e.g. `TerminateProcess` on Windows) + // to simulate an SDK-embedding host crashing, and asserts that the CLI + // still dies via the OS containment primitive alone. + std::mem::forget(client); + + loop { + std::thread::sleep(std::time::Duration::from_secs(3600)); + } +} From edf11030e5893885508504cfcbae8310b0d528f1 Mon Sep 17 00:00:00 2001 From: Derek Legenzoff Date: Tue, 1 Sep 2026 14:30:02 -0700 Subject: [PATCH 20/60] Add Auto routing tier options across all SDKs (#2437) * Add Auto routing tier options across SDKs Expose typed capi.autoTier options for create and resume across all six SDKs, including Python wire conversion. Cover canonical values and omission, and document runtime-owned persistence and resume semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2417f184-d8c9-4f2a-a27b-e15e4baea808 * Update CLI to 1.0.82-1 and regenerate Auto tier types Refresh CLI dependencies and generated RPC/session-event types across all SDKs. Reuse shared AutoTier definitions where applicable and cover typed lifecycle-event decoding, canonical values, and omission. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2417f184-d8c9-4f2a-a27b-e15e4baea808 * Strengthen CAPI forwarding coverage and remove manual Copy Use distinct create/resume WebSocket options, including alongside an unchanged Auto tier, so payload reuse cannot pass the forwarding regression test. Leave AutoTier traits owned by the Rust generator and explicitly clone the reused test value. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2417f184-d8c9-4f2a-a27b-e15e4baea808 * Fix Java auto tier README sample Add the required permission handler to the Java SessionConfig documentation sample so the doc-sample guard passes.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rerun PR checks Trigger a clean CI run after unrelated Windows E2E flakes.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson Copilot-Session: 2417f184-d8c9-4f2a-a27b-e15e4baea808 --- docs/features/session-persistence.md | 16 ++++ dotnet/src/Types.cs | 12 +++ .../test/Unit/ClientSessionLifetimeTests.cs | 88 +++++++++++++++++ .../Unit/SessionEventSerializationTests.cs | 39 ++++++++ go/client_test.go | 95 ++++++++++++------- go/session_event_serialization_test.go | 44 +++++++++ go/types.go | 20 ++++ go/zsession_events.go | 4 - java/README.md | 28 ++++++ .../java/com/github/copilot/rpc/AutoTier.java | 63 ++++++++++++ .../copilot/rpc/CapiSessionOptions.java | 31 ++++++ .../copilot/CapiSessionOptionsTest.java | 48 ++++++++++ .../copilot/SessionAutoTierEventTest.java | 67 +++++++++++++ nodejs/README.md | 1 + nodejs/src/index.ts | 1 + nodejs/src/types.ts | 14 ++- nodejs/test/client.test.ts | 70 ++++++++------ nodejs/test/session-event-types.test.ts | 28 ++++++ python/README.md | 1 + python/copilot/__init__.py | 2 + python/copilot/client.py | 25 ++++- python/test_client.py | 69 ++++++++++++-- python/test_event_forward_compatibility.py | 37 ++++++++ rust/README.md | 24 +++++ rust/src/types.rs | 74 ++++++++++++++- rust/tests/api_types_test.rs | 44 ++++++++- 26 files changed, 862 insertions(+), 83 deletions(-) create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java create mode 100644 java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 3bfff10d0f..9b0aa9434c 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -242,6 +242,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `availableTools` | Restrict which tools are available | | `excludedTools` | Disable specific tools | | `provider` | Re-provide BYOK credentials (required for BYOK sessions) | +| `capi.autoTier` | Override the persisted Auto routing preference on cold resume only | | `reasoningEffort` | Adjust reasoning effort level | | `streaming` | Enable/disable streaming responses | | `workingDirectory` | Change the working directory | @@ -253,6 +254,21 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `disabledSkills` | Skills to disable | | `infiniteSessions` | Configure infinite session behavior | +### Auto tier persistence + +With `model: "auto"`, the optional `capi.autoTier` setting selects an Auto routing preference: `efficiency`, `balance`, or `intelligence`. In Python, use `capi={"auto_tier": "balance"}`. This requires Copilot CLI `1.0.82-1` or later with V2 Auto routing; V1 Auto requests are unchanged. + +The runtime persists the selected tier, so applications do not need to resend it on every resume: + +* Omitting the tier when creating a session uses the runtime's default routing behavior. +* A cold resume restores the persisted tier. Supplying an explicit tier overrides the restored value for the new activation. +* When resuming a session already resident in the runtime, omitting the tier preserves the current selection, supplying the same tier is a no-op, and supplying a different tier is rejected. +* Older sessions without a persisted tier retain default routing behavior. + +Tier selection is not a live model-switch operation. The SDK forwards the preference; the runtime owns persistence and validation. + +The `session.start` and `session.resume` events expose the selected tier in their optional `data.autoTier` field (`data.auto_tier` in Python). When no tier is selected, the field is omitted. + ### Example: changing model on resume ```typescript diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index a7de7d85e4..8eb5e9bb2c 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2398,6 +2398,18 @@ public sealed class CapiSessionOptions /// [JsonPropertyName("enableWebSocketResponses")] public bool? EnableWebSocketResponses { get; set; } + + /// + /// Routing tier for model auto with V2 Auto. + /// + /// + /// Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto. + /// When omitted, the runtime uses its default on create and preserves the persisted or current + /// tier on resume. An explicit tier overrides the persisted tier on a cold resume; a conflicting + /// tier on a resident session resume is rejected by the runtime. + /// + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } } /// diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index bb0042efdb..a59257b3da 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -464,6 +464,94 @@ public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unse Assert.False(agent.TryGetProperty("reasoningEffort", out _)); } + public static TheoryData CapiAutoTiers => new() + { + { AutoTier.Efficiency, "efficiency", null }, + { AutoTier.Balance, "balance", null }, + { AutoTier.Intelligence, "intelligence", null }, + { AutoTier.Efficiency, "efficiency", false }, + { AutoTier.Balance, "balance", false }, + { AutoTier.Intelligence, "intelligence", false }, + }; + + [Theory] + [MemberData(nameof(CapiAutoTiers))] + public async Task SessionRequests_Serialize_CapiAutoTier(AutoTier tier, string expectedTier, bool? enableWebSocketResponses) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var capi = new CapiSessionOptions { AutoTier = tier, EnableWebSocketResponses = enableWebSocketResponses }; + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + Model = "auto", + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + await using var resumed = await client.ResumeSessionAsync("resume-with-auto-tier", new ResumeSessionConfig + { + Model = "auto", + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + foreach (var method in new[] { "session.create", "session.resume" }) + { + var request = Assert.Single(server.Requests, request => request.Method == method); + var serializedCapi = request.Params.GetProperty("capi"); + Assert.Equal(expectedTier, serializedCapi.GetProperty("autoTier").GetString()); + if (enableWebSocketResponses.HasValue) + { + Assert.Equal(enableWebSocketResponses.Value, serializedCapi.GetProperty("enableWebSocketResponses").GetBoolean()); + } + else + { + Assert.False(serializedCapi.TryGetProperty("enableWebSocketResponses", out _)); + } + } + } + + [Theory] + [InlineData(false, null)] + [InlineData(true, null)] + [InlineData(true, false)] + public async Task SessionRequests_Omit_CapiAutoTier_WhenUnset(bool includeCapi, bool? enableWebSocketResponses) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var capi = includeCapi ? new CapiSessionOptions { EnableWebSocketResponses = enableWebSocketResponses } : null; + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + Model = "auto", + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + await using var resumed = await client.ResumeSessionAsync("resume-without-auto-tier", new ResumeSessionConfig + { + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + foreach (var method in new[] { "session.create", "session.resume" }) + { + var request = Assert.Single(server.Requests, request => request.Method == method); + Assert.Equal(includeCapi, request.Params.TryGetProperty("capi", out var serializedCapi)); + if (includeCapi) + { + Assert.False(serializedCapi.TryGetProperty("autoTier", out _)); + if (enableWebSocketResponses.HasValue) + { + Assert.Equal(enableWebSocketResponses.Value, serializedCapi.GetProperty("enableWebSocketResponses").GetBoolean()); + } + else + { + Assert.Empty(serializedCapi.EnumerateObject()); + } + } + } + } + [Fact] public async Task CreateSessionAsync_Forwards_AskUserVariant() { diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 326ac3f3c7..8aea505737 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -9,6 +9,45 @@ namespace GitHub.Copilot.Test.Unit; public class SessionEventSerializationTests { + public static TheoryData AutoTiers => new() + { + { AutoTier.Efficiency, "efficiency" }, + { AutoTier.Balance, "balance" }, + { AutoTier.Intelligence, "intelligence" }, + { null, null }, + }; + + [Theory] + [MemberData(nameof(AutoTiers))] + public void SessionEvent_Deserializes_AutoTier(AutoTier? expectedTier, string? wireTier) + { + foreach (var eventType in new[] { "session.start", "session.resume" }) + { + var autoTierProperty = wireTier is null ? "" : $""", "autoTier": "{wireTier}" """; + var json = $$""" + { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": "{{eventType}}", + "data": { + "sessionId": "test-session", "version": 1, + "producer": "copilot", "copilotVersion": "1.0.82-1", + "startTime": "2026-08-28T00:00:00Z", + "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1 + {{autoTierProperty}} + } + } + """; + + var sessionEvent = SessionEvent.FromJson(json); + var actualTier = eventType == "session.start" + ? Assert.IsType(sessionEvent).Data.AutoTier + : Assert.IsType(sessionEvent).Data.AutoTier; + Assert.Equal(expectedTier, actualTier); + } + } + public static TheoryData JsonElementBackedEvents => new() { { diff --git a/go/client_test.go b/go/client_test.go index ec3924f0a9..bede29b496 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -436,42 +436,63 @@ func newRuntimeShutdownRpcPair(t *testing.T) (*jsonrpc2.Client, *jsonrpc2.Client } func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { - rpcClient, server, _ := newRuntimeShutdownRpcPair(t) - t.Cleanup(server.Stop) - client := &Client{ - client: rpcClient, - RPC: rpc.NewServerRPC(rpcClient), - sessions: make(map[string]*Session), - } + tests := []struct { + name string + capi *CapiSessionOptions + want map[string]any + }{ + {"omitted", nil, nil}, + {"empty", &CapiSessionOptions{}, map[string]any{}}, + {"websocket only", &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, map[string]any{"enableWebSocketResponses": false}}, + {"efficiency", &CapiSessionOptions{AutoTier: AutoTierEfficiency}, map[string]any{"autoTier": "efficiency"}}, + {"balance", &CapiSessionOptions{AutoTier: AutoTierBalance}, map[string]any{"autoTier": "balance"}}, + {"intelligence", &CapiSessionOptions{AutoTier: AutoTierIntelligence}, map[string]any{"autoTier": "intelligence"}}, + {"efficiency with websocket", &CapiSessionOptions{AutoTier: AutoTierEfficiency, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "efficiency", "enableWebSocketResponses": false}}, + {"balance with websocket", &CapiSessionOptions{AutoTier: AutoTierBalance, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "balance", "enableWebSocketResponses": false}}, + {"intelligence with websocket", &CapiSessionOptions{AutoTier: AutoTierIntelligence, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "intelligence", "enableWebSocketResponses": false}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } - createParams := make(chan json.RawMessage, 1) - server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - createParams <- append(json.RawMessage(nil), params...) - sessionID := sessionIDFromParams(t, params) - return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil - }) + createParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) - _, err := client.CreateSession(t.Context(), &SessionConfig{ - Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - assertCapiEnableWebSocketResponses(t, <-createParams) + _, err := client.CreateSession(t.Context(), &SessionConfig{ + Model: "auto", + Capi: tt.capi, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + assertCapiOptions(t, <-createParams, tt.want) - resumeParams := make(chan json.RawMessage, 1) - server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - resumeParams <- append(json.RawMessage(nil), params...) - return []byte(`{"sessionId":"resumed-capi","workspacePath":"/workspace"}`), nil - }) + resumeParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + return []byte(`{"sessionId":"resumed-capi","workspacePath":"/workspace"}`), nil + }) - _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{ - Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, - }) - if err != nil { - t.Fatalf("ResumeSessionWithOptions failed: %v", err) + _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{ + Model: "auto", + Capi: tt.capi, + }) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + assertCapiOptions(t, <-resumeParams, tt.want) + }) } - assertCapiEnableWebSocketResponses(t, <-resumeParams) } func TestClient_ForwardsAskUserVariantToSessionRequests(t *testing.T) { @@ -728,7 +749,7 @@ func TestClient_ForwardsNewSessionOptionsToSessionRequests(t *testing.T) { assertNewSessionOptions(t, <-resumeParams, false, false, "task", 15) } -func assertCapiEnableWebSocketResponses(t *testing.T, params json.RawMessage) { +func assertCapiOptions(t *testing.T, params json.RawMessage, want map[string]any) { t.Helper() var decoded map[string]any @@ -736,12 +757,18 @@ func assertCapiEnableWebSocketResponses(t *testing.T, params json.RawMessage) { t.Fatalf("failed to unmarshal request params: %v", err) } + if want == nil { + if _, present := decoded["capi"]; present { + t.Fatalf("expected capi to be omitted, got %v", decoded["capi"]) + } + return + } capi, ok := decoded["capi"].(map[string]any) if !ok { t.Fatalf("expected capi object in request params, got %T", decoded["capi"]) } - if capi["enableWebSocketResponses"] != false { - t.Fatalf("expected capi.enableWebSocketResponses=false, got %v", capi["enableWebSocketResponses"]) + if !reflect.DeepEqual(capi, want) { + t.Fatalf("expected capi %v, got %v", want, capi) } } diff --git a/go/session_event_serialization_test.go b/go/session_event_serialization_test.go index ee9258b225..96bf53bb5e 100644 --- a/go/session_event_serialization_test.go +++ b/go/session_event_serialization_test.go @@ -14,6 +14,50 @@ var _ SessionEventData = (*rpc.UserMessageData)(nil) var _ rpc.EmbeddedTextResourceContents = EmbeddedTextResourceContents{} var _ EmbeddedTextResourceContents = rpc.EmbeddedTextResourceContents{} +func TestSessionEventAutoTier(t *testing.T) { + for _, eventType := range []string{"session.start", "session.resume"} { + for _, tier := range []AutoTier{"", AutoTierEfficiency, AutoTierBalance, AutoTierIntelligence} { + t.Run(eventType+"/"+string(tier), func(t *testing.T) { + data := map[string]any{ + "sessionId": "test-session", "version": 1, + "producer": "copilot", "copilotVersion": "1.0.82-1", + "startTime": "2026-08-28T00:00:00Z", + "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1, + } + if tier != "" { + data["autoTier"] = tier + } + wire, err := json.Marshal(map[string]any{ + "id": "00000000-0000-0000-0000-000000000001", + "timestamp": "2026-08-28T00:00:00Z", "parentId": nil, + "type": eventType, "data": data, + }) + if err != nil { + t.Fatal(err) + } + var event SessionEvent + if err := json.Unmarshal(wire, &event); err != nil { + t.Fatal(err) + } + var actual *AutoTier + switch eventType { + case "session.start": + actual = event.Data.(*SessionStartData).AutoTier + case "session.resume": + actual = event.Data.(*SessionResumeData).AutoTier + } + if tier == "" { + if actual != nil { + t.Fatalf("expected omitted autoTier, got %v", *actual) + } + } else if actual == nil || *actual != tier { + t.Fatalf("expected autoTier %q, got %v", tier, actual) + } + }) + } + } +} + func TestSessionEventAgentIDRoundTripsKnownEvent(t *testing.T) { var event SessionEvent if err := json.Unmarshal([]byte(`{ diff --git a/go/types.go b/go/types.go index 3577731187..09bc281f22 100644 --- a/go/types.go +++ b/go/types.go @@ -2231,6 +2231,18 @@ func (p ProviderConfig) MarshalJSON() ([]byte, error) { return json.Marshal(aux) } +// AutoTier selects the routing tier for model "auto" with V2 Auto. +type AutoTier = rpc.AutoTier + +const ( + // AutoTierEfficiency selects the efficiency routing tier. + AutoTierEfficiency = rpc.AutoTierEfficiency + // AutoTierBalance selects the balance routing tier. + AutoTierBalance = rpc.AutoTierBalance + // AutoTierIntelligence selects the intelligence routing tier. + AutoTierIntelligence = rpc.AutoTierIntelligence +) + // CapiSessionOptions configures provider-scoped Copilot API (CAPI) session behavior. // // WebSocket transport is the default for the CAPI Responses API whenever the @@ -2245,6 +2257,14 @@ type CapiSessionOptions struct { // WebSocket transport. Enabled by default when the model advertises // ws:/responses support; set to Bool(false) to force HTTP Responses transport. EnableWebSocketResponses *bool `json:"enableWebSocketResponses,omitempty"` + + // AutoTier selects the routing tier for model "auto" with V2 Auto. + // Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto. + // When unset, the runtime uses its default on create and preserves the + // persisted or current tier on resume. An explicit tier overrides the + // persisted tier on a cold resume; a conflicting tier on a resident + // session resume is rejected by the runtime. + AutoTier AutoTier `json:"autoTier,omitempty"` } // AzureProviderOptions contains Azure-specific provider configuration diff --git a/go/zsession_events.go b/go/zsession_events.go index 8731009064..bdd9c918f8 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -71,7 +71,6 @@ type ( AutoModeSwitchResponse = rpc.AutoModeSwitchResponse AutopilotObjectiveChangedOperation = rpc.AutopilotObjectiveChangedOperation AutopilotObjectiveChangedStatus = rpc.AutopilotObjectiveChangedStatus - AutoTier = rpc.AutoTier BinaryAssetReference = rpc.BinaryAssetReference BinaryAssetReferenceType = rpc.BinaryAssetReferenceType BinaryAssetType = rpc.BinaryAssetType @@ -476,9 +475,6 @@ const ( AutopilotObjectiveChangedStatusCapReached = rpc.AutopilotObjectiveChangedStatusCapReached AutopilotObjectiveChangedStatusCompleted = rpc.AutopilotObjectiveChangedStatusCompleted AutopilotObjectiveChangedStatusPaused = rpc.AutopilotObjectiveChangedStatusPaused - AutoTierBalance = rpc.AutoTierBalance - AutoTierEfficiency = rpc.AutoTierEfficiency - AutoTierIntelligence = rpc.AutoTierIntelligence BinaryAssetReferenceTypeImage = rpc.BinaryAssetReferenceTypeImage BinaryAssetReferenceTypeResource = rpc.BinaryAssetReferenceTypeResource BinaryAssetTypeImage = rpc.BinaryAssetTypeImage diff --git a/java/README.md b/java/README.md index f3ba3f63a0..f01911c159 100644 --- a/java/README.md +++ b/java/README.md @@ -341,6 +341,34 @@ Chain fluent modifiers to set tool options: For design context and decision rationale, see [ADR-006](docs/adr/adr-006-tool-definition-inline.md). +## Auto routing tiers + +Use `CapiSessionOptions.setAutoTier(...)` to select `AutoTier.EFFICIENCY`, +`AutoTier.BALANCE`, or `AutoTier.INTELLIGENCE`. This option is meaningful only +with model `auto` (Auto mode V2). +It requires a runtime version that supports `capi.autoTier`. + +```java +import com.github.copilot.rpc.AutoTier; +import com.github.copilot.rpc.CapiSessionOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; + +var config = new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("auto") + .setCapi(new CapiSessionOptions().setAutoTier(AutoTier.BALANCE)); +``` + +The same options work with `ResumeSessionConfig.setCapi(...)` and can be combined +with `setEnableWebSocketResponses(false)`. The SDK omits an unset (`null`) tier: +the runtime chooses its default on create and preserves the persisted/current +tier on resume. An explicit tier overrides the persisted tier on cold resume; +the runtime rejects a conflicting tier when the session is already resident +in memory. The SDK does not choose a default or manage tier persistence. +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) +for the lifecycle rules. + ## Session Store `enableSessionStore` on `SessionConfig` enables the cross-session store for search and retrieval across sessions. When unset in the default `CopilotClientMode.COPILOT_CLI` mode, the runtime default applies (enabled). In `CopilotClientMode.EMPTY` mode, defaults to disabled. diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java new file mode 100644 index 0000000000..f9117abfb2 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Routing tier for the {@code auto} model with Auto mode V2. + * + * @see CapiSessionOptions#setAutoTier(AutoTier) + */ +public enum AutoTier { + + /** Prioritize efficiency. */ + EFFICIENCY("efficiency"), + + /** Balance efficiency and intelligence. */ + BALANCE("balance"), + + /** Prioritize intelligence. */ + INTELLIGENCE("intelligence"); + + private final String value; + + AutoTier(String value) { + this.value = value; + } + + /** + * Returns the JSON value for this routing tier. + * + * @return the string value used in JSON serialization + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Deserializes a JSON string into its routing tier. + * + * @param value + * the JSON string value + * @return the matching tier, or {@code null} if value is {@code null} + * @throws IllegalArgumentException + * if the value does not match a known routing tier + */ + @JsonCreator + public static AutoTier fromValue(String value) { + if (value == null) { + return null; + } + for (AutoTier tier : values()) { + if (tier.value.equals(value)) { + return tier; + } + } + throw new IllegalArgumentException("Unknown AutoTier value: " + value); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java index d94d59f67b..e401762302 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java @@ -29,9 +29,40 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class CapiSessionOptions { + @JsonProperty("autoTier") + private AutoTier autoTier; + @JsonProperty("enableWebSocketResponses") private Boolean enableWebSocketResponses; + /** + * Gets the routing tier for the {@code auto} model (Auto mode V2). + * + * @return the explicit tier, or {@code null} to leave tier selection to the + * runtime + */ + public AutoTier getAutoTier() { + return autoTier; + } + + /** + * Sets the routing tier, meaningful only with model {@code auto} (Auto mode + * V2). Requires a runtime version that supports {@code capi.autoTier}. + *

+ * When omitted, the runtime chooses its default on create and preserves the + * persisted or current tier on resume. An explicit tier overrides the persisted + * tier on cold resume; the runtime rejects a conflicting tier when resuming a + * session already resident in memory. + * + * @param autoTier + * the routing tier, or {@code null} to omit it from the request + * @return this config for method chaining + */ + public CapiSessionOptions setAutoTier(AutoTier autoTier) { + this.autoTier = autoTier; + return this; + } + /** * Gets whether CAPI Responses API WebSocket transport is enabled. * diff --git a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java index 17e8f131f7..dccb4e9add 100644 --- a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java @@ -9,12 +9,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import com.fasterxml.jackson.databind.JsonNode; +import com.github.copilot.rpc.AutoTier; import com.github.copilot.rpc.CapiSessionOptions; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.SessionConfig; @@ -29,6 +33,7 @@ void defaultsAreNull() { var capi = new CapiSessionOptions(); assertNull(capi.getEnableWebSocketResponses()); + assertNull(capi.getAutoTier()); } @Test @@ -37,6 +42,8 @@ void fluentSetterReturnsSameInstance() { assertSame(capi, capi.setEnableWebSocketResponses(true)); assertEquals(Boolean.TRUE, capi.getEnableWebSocketResponses()); + assertSame(capi, capi.setAutoTier(AutoTier.BALANCE)); + assertEquals(AutoTier.BALANCE, capi.getAutoTier()); } @Test @@ -46,6 +53,7 @@ void serializesEnableWebSocketResponses() { JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); assertTrue(json.get("enableWebSocketResponses").asBoolean()); + assertTrue(json.path("autoTier").isMissingNode()); } @Test @@ -55,6 +63,44 @@ void omitsUnsetEnableWebSocketResponses() { JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); assertTrue(json.path("enableWebSocketResponses").isMissingNode()); + assertTrue(json.path("autoTier").isMissingNode()); + assertEquals(0, json.size()); + } + + @ParameterizedTest + @CsvSource({"EFFICIENCY,efficiency", "BALANCE,balance", "INTELLIGENCE,intelligence"}) + void autoTierCanonicalValuesRoundTripAndForward(AutoTier tier, String value) throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var capi = new CapiSessionOptions().setAutoTier(tier); + JsonNode json = mapper.valueToTree(capi); + assertEquals(value, json.get("autoTier").asText()); + assertEquals(1, json.size()); + assertEquals(tier, mapper.treeToValue(json, CapiSessionOptions.class).getAutoTier()); + + capi.setEnableWebSocketResponses(false); + var create = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setModel("auto").setCapi(capi), + "session-1"); + var resume = SessionRequestBuilder.buildResumeRequest("session-1", new ResumeSessionConfig().setCapi(capi)); + for (Object request : new Object[]{create, resume}) { + JsonNode requestJson = mapper.valueToTree(request); + assertEquals(value, requestJson.get("capi").get("autoTier").asText()); + assertFalse(requestJson.get("capi").get("enableWebSocketResponses").asBoolean()); + assertEquals(2, requestJson.get("capi").size()); + } + } + + @Test + void autoTierRejectsNoncanonicalValues() { + for (String value : new String[]{"balanced", "Balance", "unknown"}) { + assertThrows(IllegalArgumentException.class, () -> AutoTier.fromValue(value)); + } + assertNull(AutoTier.fromValue(null)); + } + + @Test + void clearingAutoTierOmitsIt() { + var capi = new CapiSessionOptions().setAutoTier(AutoTier.BALANCE).setAutoTier(null); + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); assertEquals(0, json.size()); } @@ -67,6 +113,7 @@ void createRequestIncludesCapiWhenSet() { assertNotNull(request.getCapi()); assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean()); + assertTrue(json.get("capi").path("autoTier").isMissingNode()); } @Test @@ -89,6 +136,7 @@ void resumeRequestIncludesCapiWhenSet() { assertNotNull(request.getCapi()); assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean()); + assertTrue(json.get("capi").path("autoTier").isMissingNode()); } @Test diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java new file mode 100644 index 0000000000..5213cdbb8d --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.github.copilot.generated.AutoTier; +import com.github.copilot.generated.SessionEvent; +import com.github.copilot.generated.SessionResumeEvent; +import com.github.copilot.generated.SessionStartEvent; + +/** + * Verifies auto routing preferences on generated session lifecycle events. + */ +class SessionAutoTierEventTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + @ParameterizedTest + @CsvSource({"session.start,EFFICIENCY,efficiency", "session.start,BALANCE,balance", + "session.start,INTELLIGENCE,intelligence", "session.resume,EFFICIENCY,efficiency", + "session.resume,BALANCE,balance", "session.resume,INTELLIGENCE,intelligence"}) + void canonicalAutoTierRoundTrips(String type, AutoTier tier, String value) throws Exception { + String json = """ + {"type":"%s","data":{"selectedModel":"auto","autoTier":"%s"}} + """.formatted(type, value); + + var event = MAPPER.readValue(json, SessionEvent.class); + assertEquals(tier, autoTier(event, type)); + String serialized = MAPPER.writeValueAsString(event); + assertEquals(value, MAPPER.readTree(serialized).path("data").path("autoTier").asText()); + assertEquals(tier, autoTier(MAPPER.readValue(serialized, SessionEvent.class), type)); + } + + @ParameterizedTest + @ValueSource(strings = {"session.start", "session.resume"}) + void missingOrNullAutoTierRemainsOptional(String type) throws Exception { + for (String data : new String[]{"{}", "{\"autoTier\":null}"}) { + String json = """ + {"type":"%s","data":%s} + """.formatted(type, data); + + var event = MAPPER.readValue(json, SessionEvent.class); + assertNull(autoTier(event, type)); + var serialized = MAPPER.readTree(MAPPER.writeValueAsString(event)); + assertFalse(serialized.path("data").has("autoTier")); + } + } + + private static AutoTier autoTier(SessionEvent event, String type) { + if ("session.start".equals(type)) { + return assertInstanceOf(SessionStartEvent.class, event).getData().autoTier(); + } + return assertInstanceOf(SessionResumeEvent.class, event).getData().autoTier(); + } +} diff --git a/nodejs/README.md b/nodejs/README.md index 5886df234f..57a8bf484b 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -132,6 +132,7 @@ Create a new conversation session. - `sessionId?: string` - Custom session ID. - `model?: string` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** +- `capi?: CapiSessionOptions` - Copilot API options. With `model: "auto"`, set `autoTier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics. - `reasoningEffort?: "low" | "medium" | "high" | "xhigh" | "max"` - Reasoning effort level for models that support it. Use `listModels()` to check which models support this option. - `tools?: Tool[]` - Custom tools exposed to the CLI. Tools without `handler` are declaration-only and must be resolved via pending tool-call RPCs. - `systemMessage?: SystemMessageConfig` - System message customization (see below) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 8b9095e0ba..11e742bee7 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -120,6 +120,7 @@ export type { ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, + AutoTier, CapiSessionOptions, ModelCapabilities, ModelCapabilitiesOverride, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 85c77f8340..cd211fd9f5 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -11,6 +11,7 @@ import type { Canvas } from "./canvas.js"; import type { SessionFsProvider } from "./sessionFsProvider.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; import type { + AutoTier, PermissionRequest as GeneratedPermissionRequest, PermissionRequestedData as GeneratedPermissionRequestedData, PermissionRequestedEvent as GeneratedPermissionRequestedEvent, @@ -72,7 +73,7 @@ export type { export type SessionEvent = | Exclude | PermissionRequestedEvent; -export type { ReasoningSummary } from "./generated/session-events.js"; +export type { AutoTier, ReasoningSummary } from "./generated/session-events.js"; export type { SessionFsProvider } from "./sessionFsProvider.js"; export { createSessionFsAdapter } from "./sessionFsProvider.js"; export type { SessionFsFileInfo } from "./sessionFsProvider.js"; @@ -2129,6 +2130,17 @@ export interface FactoryMeta { * provider-level choices are conceptually per-provider rather than global. */ export interface CapiSessionOptions { + /** + * Routing preference used when the session model is `auto`. + * Requires a runtime with Auto tier support and V2 Auto routing. + * + * When omitted on create, the runtime uses its default routing behavior. + * The runtime persists this preference across cold resume; an explicit tier + * on cold resume overrides the persisted value. For an already-resident + * session, omission preserves the current tier and a different tier is rejected. + */ + autoTier?: AutoTier; + /** * Whether to use the WebSocket transport for the CAPI Responses API. * diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index a08e4a6e5a..829d69f2e5 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -12,6 +12,7 @@ import { createCanvas, DisableBypassPermissionsModes, RuntimeConnection, + type CapiSessionOptions, type GitHubTelemetryNotification, type ManagedSettings, type ModelInfo, @@ -1364,37 +1365,50 @@ describe("CopilotClient", () => { expect(resumePayload.featureFlags).toEqual(featureFlags); }); - it("forwards capi options in session.create and session.resume", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => stopClient(client)); + it.each([ + undefined, + {}, + { enableWebSocketResponses: false }, + { enableWebSocketResponses: true }, + { autoTier: "efficiency" }, + { autoTier: "balance" }, + { autoTier: "intelligence" }, + { autoTier: "balance", enableWebSocketResponses: false }, + ] satisfies (CapiSessionOptions | undefined)[])( + "forwards capi options %j in session.create and session.resume", + async (capi) => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); - const spy = vi - .spyOn((client as any).connection!, "sendRequest") - .mockImplementation(async (method: string, params: any) => { - if (method === "session.create") return { sessionId: params.sessionId }; - if (method === "session.resume") return { sessionId: params.sessionId }; - throw new Error(`Unexpected method: ${method}`); - }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); - const session = await client.createSession({ - onPermissionRequest: approveAll, - capi: { enableWebSocketResponses: false }, - }); - await client.resumeSession(session.sessionId, { - onPermissionRequest: approveAll, - capi: { enableWebSocketResponses: false }, - }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "auto", + capi, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + capi, + }); - const createPayload = spy.mock.calls.find( - ([method]) => method === "session.create" - )![1] as any; - const resumePayload = spy.mock.calls.find( - ([method]) => method === "session.resume" - )![1] as any; - expect(createPayload.capi).toEqual({ enableWebSocketResponses: false }); - expect(resumePayload.capi).toEqual({ enableWebSocketResponses: false }); - }); + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(JSON.parse(JSON.stringify(createPayload)).capi).toEqual(capi); + expect(JSON.parse(JSON.stringify(resumePayload)).capi).toEqual(capi); + } + ); it("forwards pluginDirectories and largeOutput in session.create and session.resume", async () => { const client = new CopilotClient(); diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts index 5c41f2216a..93edebfc80 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -21,6 +21,8 @@ import type { FactoryAgentOptions as WireFactoryAgentOptions } from "../src/gene import type { // The aggregate union; must still resolve via the package root. SessionEvent, + AutoTier, + CapiSessionOptions, PermissionRequest, PermissionRequestedData, PermissionRequestedEvent, @@ -128,6 +130,32 @@ type _PermissionRequestedEventStaysAlignedWithSessionEventUnion = _AssertEqual< const _permissionRequestedEventAlignmentCheck: _PermissionRequestedEventStaysAlignedWithSessionEventUnion = true; describe("Session event type exports (#1156)", () => { + it.each(["efficiency", "balance", "intelligence", undefined] satisfies ( + | AutoTier + | undefined + )[])("exposes Auto tier %s on start and resume data", (autoTier) => { + const start: StartData = { + copilotVersion: "1.0.82-1", + producer: "copilot-agent", + sessionId: "session-1", + startTime: "2026-08-28T00:00:00Z", + version: 1, + autoTier, + }; + const resume: ResumeData = { + eventCount: 1, + resumeTime: "2026-08-28T00:01:00Z", + autoTier, + }; + const capi: CapiSessionOptions = { autoTier: start.autoTier }; + expect(capi.autoTier).toBe(autoTier); + expect(resume.autoTier).toBe(autoTier); + if (autoTier === undefined) { + expect(JSON.parse(JSON.stringify(start))).not.toHaveProperty("autoTier"); + expect(JSON.parse(JSON.stringify(resume))).not.toHaveProperty("autoTier"); + } + }); + it("exposes the headline ToolExecutionStartData type with a usable shape", () => { // This is the specific type called out in issue #1156. The annotation // is the compile-time API-surface check; these assertions only validate diff --git a/python/README.md b/python/README.md index 5cb1dc03c9..359026df41 100644 --- a/python/README.md +++ b/python/README.md @@ -278,6 +278,7 @@ finally: These are passed as keyword arguments to `create_session()`: - `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** +- `capi` (CapiSessionOptions): Copilot API options. With `model="auto"`, set `auto_tier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics. - `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh", "max"). Use `list_models()` to check which models support this option. - `session_id` (str): Custom session ID - `tools` (list): Custom tools exposed to the CLI. Tools with `handler=None` are declaration-only and must be resolved via pending tool-call RPCs. diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 4b3b9901ed..5325b937c5 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -30,6 +30,7 @@ ) from .client import ( AskUserVariant, + AutoTier, CapiSessionOptions, ChildProcessRuntimeConnection, CloudSessionOptions, @@ -231,6 +232,7 @@ "AutoModeSwitchRequest", "AutoModeSwitchResponse", "AskUserVariant", + "AutoTier", "BUILTIN_TOOLS_ISOLATED", "CanvasAction", "CanvasDeclaration", diff --git a/python/copilot/client.py b/python/copilot/client.py index e50e5fa307..a15c1a569e 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -263,9 +263,23 @@ def _exp_assignment_response_to_dict( return wire +AutoTier = Literal["efficiency", "balance", "intelligence"] +"""Routing preference used when the session model is ``auto``.""" + + class CapiSessionOptions(TypedDict, total=False): """Provider-scoped Copilot API (CAPI) session options.""" + auto_tier: AutoTier + """Routing preference used when the session model is ``auto``. + + Requires a runtime with Auto tier support and V2 Auto routing. When omitted + on create, the runtime uses its default routing behavior. The runtime persists + this preference across cold resume; an explicit tier on cold resume overrides + the persisted value. For an already-resident session, omission preserves the + current tier and a different tier is rejected. + """ + enable_web_socket_responses: bool """Whether to use WebSocket transport for the CAPI Responses API. @@ -292,6 +306,8 @@ def _cloud_session_options_to_dict(options: CloudSessionOptions) -> dict[str, An def _capi_session_options_to_wire(options: CapiSessionOptions) -> dict[str, Any]: wire: dict[str, Any] = {} + if "auto_tier" in options: + wire["autoTier"] = options["auto_tier"] if "enable_web_socket_responses" in options: wire["enableWebSocketResponses"] = options["enable_web_socket_responses"] return wire @@ -2298,7 +2314,9 @@ async def create_session( hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. - capi: CAPI provider-scoped options. WebSocket transport is the + capi: CAPI provider-scoped options. Set ``auto_tier`` to ``efficiency``, + ``balance``, or ``intelligence`` to select an Auto routing preference + on a runtime with Auto tier support. WebSocket transport is the default for the CAPI Responses API whenever the model advertises the ``ws:/responses`` endpoint. Set ``enable_web_socket_responses=False`` to force the HTTP @@ -3076,7 +3094,10 @@ async def resume_session( hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. - capi: CAPI provider-scoped options. WebSocket transport is the + capi: CAPI provider-scoped options. Omit ``auto_tier`` to preserve the + current or persisted Auto routing preference. An explicit tier + overrides it on cold resume, but cannot change it on an + already-resident session. WebSocket transport is the default for the CAPI Responses API whenever the model advertises the ``ws:/responses`` endpoint. Set ``enable_web_socket_responses=False`` to force the HTTP diff --git a/python/test_client.py b/python/test_client.py index e475dba0e2..d1c6ebe1d7 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -1147,7 +1147,56 @@ async def mock_request(method, params, **kwargs): await client.force_stop() @pytest.mark.asyncio - async def test_create_and_resume_session_forward_capi_options(self): + @pytest.mark.parametrize( + ("create_capi", "resume_capi", "expected_create", "expected_resume"), + [ + (None, None, None, None), + ({}, {}, {}, {}), + ( + {"enable_web_socket_responses": False}, + {"enable_web_socket_responses": True}, + {"enableWebSocketResponses": False}, + {"enableWebSocketResponses": True}, + ), + ( + {"enable_web_socket_responses": True}, + {"enable_web_socket_responses": False}, + {"enableWebSocketResponses": True}, + {"enableWebSocketResponses": False}, + ), + ( + {"auto_tier": "efficiency"}, + {"auto_tier": "efficiency"}, + {"autoTier": "efficiency"}, + {"autoTier": "efficiency"}, + ), + ( + {"auto_tier": "balance"}, + {"auto_tier": "balance"}, + {"autoTier": "balance"}, + {"autoTier": "balance"}, + ), + ( + {"auto_tier": "intelligence"}, + {"auto_tier": "intelligence"}, + {"autoTier": "intelligence"}, + {"autoTier": "intelligence"}, + ), + ( + {"auto_tier": "balance", "enable_web_socket_responses": False}, + {"auto_tier": "balance", "enable_web_socket_responses": True}, + {"autoTier": "balance", "enableWebSocketResponses": False}, + {"autoTier": "balance", "enableWebSocketResponses": True}, + ), + ], + ) + async def test_create_and_resume_session_forward_capi_options( + self, + create_capi: CapiSessionOptions | None, + resume_capi: CapiSessionOptions | None, + expected_create: dict[str, object] | None, + expected_resume: dict[str, object] | None, + ): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -1164,11 +1213,9 @@ async def mock_request(method, params, **kwargs): return {} client._client.request = mock_request - create_capi: CapiSessionOptions = {"enable_web_socket_responses": False} - resume_capi: CapiSessionOptions = {"enable_web_socket_responses": True} - session = await client.create_session( on_permission_request=PermissionHandler.approve_all, + model="auto", capi=create_capi, ) await client.resume_session( @@ -1177,12 +1224,14 @@ async def mock_request(method, params, **kwargs): capi=resume_capi, ) - assert captured["session.create"]["capi"] == { - "enableWebSocketResponses": False, - } - assert captured["session.resume"]["capi"] == { - "enableWebSocketResponses": True, - } + for method, expected in ( + ("session.create", expected_create), + ("session.resume", expected_resume), + ): + if expected is None: + assert "capi" not in captured[method] + else: + assert captured[method]["capi"] == expected finally: await client.force_stop() diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 2e8015a97d..00cad43217 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -14,6 +14,7 @@ from copilot.session_events import ( AttachmentGitHubReferenceType, + AutoTier, Data, ElicitationCompletedAction, ElicitationRequestedMode, @@ -24,6 +25,8 @@ PermissionRequestMemoryAction, SessionEventType, SessionManagedSettingsResolvedData, + SessionResumeData, + SessionStartData, SessionTaskCompleteData, UserMessageAgentMode, session_event_from_dict, @@ -34,6 +37,40 @@ class TestEventForwardCompatibility: """Test forward compatibility for unknown event types.""" + @pytest.mark.parametrize("event_type", ["session.start", "session.resume"]) + @pytest.mark.parametrize("tier", ["efficiency", "balance", "intelligence", None]) + def test_auto_tier_lifecycle_events_round_trip(self, event_type, tier): + timestamp = "2026-08-28T00:00:00Z" + data = ( + { + "copilotVersion": "1.0.82-1", + "producer": "copilot-agent", + "sessionId": str(uuid4()), + "startTime": timestamp, + "version": 1, + } + if event_type == "session.start" + else {"eventCount": 1, "resumeTime": timestamp} + ) + if tier is not None: + data["autoTier"] = tier + event = session_event_from_dict( + { + "id": str(uuid4()), + "timestamp": timestamp, + "parentId": None, + "type": event_type, + "data": data, + } + ) + assert isinstance(event.data, (SessionStartData, SessionResumeData)) + assert event.data.auto_tier == (AutoTier(tier) if tier is not None else None) + serialized = session_event_to_dict(event)["data"] + if tier is None: + assert "autoTier" not in serialized + else: + assert serialized["autoTier"] == tier + def test_session_usage_info_is_recognized(self): """The session.usage_info event type should be in the enum.""" assert SessionEventType.SESSION_USAGE_INFO.value == "session.usage_info" diff --git a/rust/README.md b/rust/README.md index c05746a30e..b9a1f5b1bc 100644 --- a/rust/README.md +++ b/rust/README.md @@ -362,6 +362,30 @@ provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. +### Auto routing tiers + +Use `CapiSessionOptions::with_auto_tier` to select `AutoTier::Efficiency`, +`AutoTier::Balance`, or `AutoTier::Intelligence`. This option is meaningful only +with model `auto` (Auto mode V2). +It requires a runtime version that supports `capi.autoTier`. + +```rust +use github_copilot_sdk::{AutoTier, CapiSessionOptions, SessionConfig}; + +let config = SessionConfig::default() + .with_model("auto") + .with_capi(CapiSessionOptions::new().with_auto_tier(AutoTier::Balance)); +``` + +The same options work with `ResumeSessionConfig::with_capi` and can be combined +with `with_enable_web_socket_responses(false)`. The SDK omits an unset tier: +the runtime chooses its default on create and preserves the persisted/current +tier on resume. An explicit tier overrides the persisted tier on cold resume; +the runtime rejects a conflicting tier when the session is already resident +in memory. The SDK does not choose a default or manage tier persistence. +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) +for the lifecycle rules. + ### Session Hooks Hooks intercept CLI behavior at lifecycle points — tool use, prompt submission, session start/end, and errors. Install a `SessionHooks` impl with [`SessionConfig::with_hooks`] — the SDK auto-enables `hooks` in `SessionConfig` when one is set. diff --git a/rust/src/types.rs b/rust/src/types.rs index 89906f9627..ee3ac3df26 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -21,6 +21,8 @@ pub use crate::copilot_request_handler::{ CopilotWebSocketResponse, WebSocketTransform, forward_http, }; use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance}; +/// Routing tier for the `auto` model with Auto mode V2. +pub use crate::generated::session_events::AutoTier; use crate::generated::session_events::ReasoningSummary; /// Context window tier for models that support tiered context windows. pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig}; @@ -1417,6 +1419,16 @@ impl ProviderConfig { #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct CapiSessionOptions { + /// Routing tier, meaningful only with model `auto` (Auto mode V2). + /// Requires a runtime version that supports `capi.autoTier`. + /// + /// When omitted, the runtime chooses its default on create and preserves + /// the persisted or current tier on resume. An explicit tier overrides the + /// persisted tier on cold resume; the runtime rejects a conflicting tier + /// when resuming a session already resident in memory. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, + /// Whether to use WebSocket transport for CAPI Responses API calls. /// /// When `Some(false)`, the runtime uses HTTP Responses transport even if @@ -1432,6 +1444,12 @@ impl CapiSessionOptions { Self::default() } + /// Set the routing tier for the `auto` model (Auto mode V2). + pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self { + self.auto_tier = Some(auto_tier); + self + } + /// Set whether to use WebSocket transport for CAPI Responses API calls. pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self { self.enable_web_socket_responses = Some(enable); @@ -6034,9 +6052,9 @@ mod tests { use super::{ AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, - AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState, - CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry, - ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, + AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, + ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, + ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, @@ -7322,6 +7340,56 @@ mod tests { let unset = CapiSessionOptions::new(); let wire_unset = serde_json::to_value(&unset).unwrap(); assert!(wire_unset.get("enableWebSocketResponses").is_none()); + assert!(wire_unset.get("autoTier").is_none()); + assert_eq!(wire_unset, json!({})); + } + + #[test] + fn capi_auto_tier_canonical_values_round_trip_and_forward() { + for (tier, value) in [ + (AutoTier::Efficiency, "efficiency"), + (AutoTier::Balance, "balance"), + (AutoTier::Intelligence, "intelligence"), + ] { + let exported: crate::AutoTier = tier.clone(); + let capi = CapiSessionOptions::new().with_auto_tier(exported); + assert_eq!(capi.auto_tier, Some(tier)); + assert_eq!( + serde_json::to_value(&capi).unwrap(), + json!({"autoTier": value}) + ); + assert_eq!( + serde_json::from_value::(json!({"autoTier": value})).unwrap(), + capi + ); + + let capi = capi.with_enable_web_socket_responses(false); + let expected = json!({"autoTier": value, "enableWebSocketResponses": false}); + let (create, _) = SessionConfig::default() + .with_model("auto") + .with_capi(capi.clone()) + .into_wire(Some(SessionId::from("capi-create"))) + .unwrap(); + assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected); + + let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(capi) + .into_wire() + .unwrap(); + assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected); + } + } + + #[test] + fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() { + for value in ["balanced", "Balance", "unknown"] { + assert_eq!( + serde_json::from_value::(json!(value)).unwrap(), + AutoTier::Unknown + ); + } + let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); + assert_eq!(capi.auto_tier, None); } #[test] diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index 9b86b1367a..8ed40e7c76 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -3,11 +3,53 @@ #![allow(clippy::unwrap_used)] +use github_copilot_sdk::AutoTier; use github_copilot_sdk::rpc::{ Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest, }; -use github_copilot_sdk::session_events::{PermissionRequest, PermissionRequestedData}; +use github_copilot_sdk::session_events::{ + PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent, +}; + +#[test] +fn session_events_deserialize_auto_tier() { + for event_type in ["session.start", "session.resume"] { + for (tier, wire_tier) in [ + (Some(AutoTier::Efficiency), Some("efficiency")), + (Some(AutoTier::Balance), Some("balance")), + (Some(AutoTier::Intelligence), Some("intelligence")), + (None, None), + ] { + let mut wire = serde_json::json!({ + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": event_type, + "data": { + "sessionId": "test-session", "version": 1, + "producer": "copilot", "copilotVersion": "1.0.82-1", + "startTime": "2026-08-28T00:00:00Z", + "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1 + } + }); + if let Some(wire_tier) = wire_tier { + wire["data"]["autoTier"] = serde_json::json!(wire_tier); + } + let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); + let actual: Option = match event.payload { + SessionEventData::SessionStart(data) if event_type == "session.start" => { + data.auto_tier + } + SessionEventData::SessionResume(data) if event_type == "session.resume" => { + data.auto_tier + } + _ => panic!("expected {event_type}"), + }; + assert_eq!(actual, tier); + } + } +} #[test] fn extension_running_has_expected_status_and_source() { From 2980c7828d35754bfc2b334831efec309ab8a2eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:41:08 +0000 Subject: [PATCH 21/60] Update @github/copilot to 1.0.83-1 (#2467) * Update @github/copilot to 1.0.83-1 - Updated nodejs and test harness dependencies - Re-ran code generators - Formatted generated code * Fix compatibility tests for Copilot schema update Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Stephen Toub Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 64 +++++++++++++++-- dotnet/src/Generated/SessionEvents.cs | 47 ++++++++++-- go/rpc/zrpc.go | 48 ++++++++++++- go/rpc/zrpc_encoding.go | 19 +++++ go/rpc/zsession_encoding.go | 6 ++ go/rpc/zsession_events.go | 26 +++++-- go/zsession_events.go | 3 + java/pom.xml | 2 +- java/scripts/codegen/package-lock.json | 72 +++++++++---------- java/scripts/codegen/package.json | 2 +- .../AssistantMessageReasoningBlocks.java | 2 +- .../ManagedSettingsResolvedSource.java | 2 + .../copilot/generated/SessionEvent.java | 2 + .../SessionManagedSettingsResolvedEvent.java | 6 +- .../SessionModeNoticeDeliveredEvent.java | 43 +++++++++++ .../rpc/CatalogNetworkFailureError.java | 7 ++ .../rpc/CatalogNetworkFailureReason.java | 6 ++ .../generated/rpc/CatalogSearchParams.java | 2 +- .../generated/rpc/FactoryRunResult.java | 4 +- .../generated/rpc/ModelBillingPromo.java | 4 +- .../rpc/SessionFactoryCancelResult.java | 4 +- .../rpc/SessionFactoryGetRunResult.java | 4 +- .../rpc/SessionFactoryRunFromToolResult.java | 4 +- .../rpc/SessionFactoryRunResult.java | 4 +- ...SessionModelApplyStartupOverlayParams.java | 2 + .../rpc/SlashCommandCompletedResult.java | 7 ++ .../rpc/GeneratedRpcRecordsCoverageTest.java | 3 +- nodejs/package-lock.json | 54 +++++++------- nodejs/package.json | 2 +- nodejs/samples/package-lock.json | 2 +- nodejs/src/generated/rpc.ts | 37 +++++++++- nodejs/src/generated/session-events.ts | 55 ++++++++++++-- python/copilot/generated/rpc.py | 71 +++++++++++++++--- python/copilot/generated/session_events.py | 40 ++++++++++- python/test_event_forward_compatibility.py | 1 + rust/src/generated/api_types.rs | 50 +++++++++++-- rust/src/generated/session_events.rs | 29 ++++++-- test/harness/package-lock.json | 54 +++++++------- test/harness/package.json | 2 +- 39 files changed, 639 insertions(+), 153 deletions(-) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.java diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index c7be742ee2..4379a2c122 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -118,6 +118,10 @@ public sealed class ModelBillingPromo ///

Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. [JsonPropertyName("message")] public string? Message { get; set; } + + /// Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + [JsonPropertyName("showBanner")] + public bool? ShowBanner { get; set; } } /// Long context tier pricing (available for models with extended context windows). @@ -1841,6 +1845,11 @@ public partial class McpPlanInstallResultNetworkFailure : McpPlanInstallResult [JsonPropertyName("reason")] public required CatalogNetworkFailureReason Reason { get; set; } + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("retryAfterSeconds")] + public int? RetryAfterSeconds { get; set; } + /// HTTP status code, when the failure was a rejected response. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("statusCode")] @@ -2655,6 +2664,11 @@ public partial class CatalogSearchResultNetworkFailure : CatalogSearchResult [JsonPropertyName("reason")] public required CatalogNetworkFailureReason Reason { get; set; } + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("retryAfterSeconds")] + public int? RetryAfterSeconds { get; set; } + /// HTTP status code, when the failure was a rejected response. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("statusCode")] @@ -2762,7 +2776,7 @@ internal sealed class CatalogSearchRequest [JsonPropertyName("limit")] public int? Limit { get; set; } - /// Free-text search query. Never written to logs or telemetry. + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. [RegularExpression("\\S")] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] [MinLength(1)] @@ -6244,6 +6258,7 @@ internal sealed class CanvasProviderUnregisterRequest [JsonDerivedType(typeof(FactoryRunFailureFactoryResumeDeclined), "factory_resume_declined")] [JsonDerivedType(typeof(FactoryRunFailureFactoryDurableFailure), "factory_durable_failure")] [JsonDerivedType(typeof(FactoryRunFailureFactoryAccountingIncomplete), "factory_accounting_incomplete")] +[JsonDerivedType(typeof(FactoryRunFailureFactoryProviderDisconnected), "factory_provider_disconnected")] public partial class FactoryRunFailure { /// The type discriminator. @@ -6329,15 +6344,33 @@ public partial class FactoryRunFailureFactoryAccountingIncomplete : FactoryRunFa public required string RunId { get; set; } } +/// The extension that owns the factory disconnected while the run was executing, so the host halted it. The run's journaled subagent results are preserved so a resume can reuse them. +/// The factory_provider_disconnected variant of . +[Experimental(Diagnostics.Experimental)] +public partial class FactoryRunFailureFactoryProviderDisconnected : FactoryRunFailure +{ + /// + [JsonIgnore] + public override string Type => "factory_provider_disconnected"; + + /// Factory run identifier. + [JsonPropertyName("runId")] + public required string RunId { get; set; } +} + /// Complete current or terminal factory run envelope. [Experimental(Diagnostics.Experimental)] public sealed class FactoryRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + [JsonPropertyName("attempt")] + public long? Attempt { get; set; } + /// Error message for an errored run. [JsonPropertyName("error")] public string? Error { get; set; } - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. [JsonPropertyName("failure")] public FactoryRunFailure? Failure { get; set; } @@ -7479,6 +7512,10 @@ internal sealed class ModelApplyStartupOverlayRequest [JsonPropertyName("deviceManagedModel")] public string? DeviceManagedModel { get; set; } + /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. + [JsonPropertyName("policyHelperModel")] + public string? PolicyHelperModel { get; set; } + /// Context tier selected by repository settings, when configured. [JsonPropertyName("repoContextTier")] public string? RepoContextTier { get; set; } @@ -13100,6 +13137,11 @@ public partial class SlashCommandInvocationResultCompleted : SlashCommandInvocat [JsonPropertyName("message")] public string? Message { get; set; } + /// Optional target session mode applied without submitting an agent prompt. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mode")] + public SessionMode? Mode { get; set; } + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("runtimeSettingsChanged")] @@ -20285,7 +20327,16 @@ public CatalogNetworkFailureReason(string value) /// The connection was refused or reset. public static CatalogNetworkFailureReason ConnectionRefused { get; } = new("connection-refused"); - /// The authority returned a status the runtime treats as a failure. + /// The configured proxy returned 407 and requires authentication. + public static CatalogNetworkFailureReason ProxyAuthenticationRequired { get; } = new("proxy-authentication-required"); + + /// The authority rate-limited requests and supplied or implied a bounded cooldown. + public static CatalogNetworkFailureReason RateLimited { get; } = new("rate-limited"); + + /// The authority returned a transient 5xx response. + public static CatalogNetworkFailureReason ServiceUnavailable { get; } = new("service-unavailable"); + + /// The authority returned another status the runtime treats as a failure. public static CatalogNetworkFailureReason HttpStatus { get; } = new("http-status"); /// The response exceeded the permitted size. @@ -30014,7 +30065,7 @@ internal ServerCatalogApi(JsonRpc rpc) /// Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. /// Protocol version and capabilities the caller requires. - /// Free-text search query. Never written to logs or telemetry. + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. /// Maximum number of candidates to return. Defaults to 10 when omitted. /// Restrict results to these candidate kinds. When omitted, every kind the runtime supports is searched. /// The to monitor for cancellation requests. The default is . @@ -31967,6 +32018,7 @@ public async Task SwitchToAsync(string modelId, string? rea /// Resolves and applies organization-managed and repository model overlays. /// Model required by device-managed policy, when configured. /// Model required by server-managed policy, when configured. + /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. /// Model selected by repository settings, when configured. /// Reasoning effort selected by repository settings, when configured. /// Context tier selected by repository settings, when configured. @@ -31974,11 +32026,11 @@ public async Task SwitchToAsync(string modelId, string? rea /// Whether the overlay is being applied while resuming a deferred session. /// The to monitor for cancellation requests. The default is . /// The model identifier active on the session after the switch. - internal async Task ApplyStartupOverlayAsync(string? deviceManagedModel = null, string? serverManagedModel = null, string? repoModel = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? cliModel = null, bool? deferredResume = null, CancellationToken cancellationToken = default) + internal async Task ApplyStartupOverlayAsync(string? deviceManagedModel = null, string? serverManagedModel = null, string? policyHelperModel = null, string? repoModel = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? cliModel = null, bool? deferredResume = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, RepoModel = repoModel, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, CliModel = cliModel, DeferredResume = deferredResume }; + var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, PolicyHelperModel = policyHelperModel, RepoModel = repoModel, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, CliModel = cliModel, DeferredResume = deferredResume }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.applyStartupOverlay", [request], cancellationToken); } diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 6bfe202bff..170e83027a 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -113,6 +113,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionMcpServerStatusChangedEvent), "session.mcp_server_status_changed")] [JsonDerivedType(typeof(SessionMcpServersLoadedEvent), "session.mcp_servers_loaded")] [JsonDerivedType(typeof(SessionModeChangedEvent), "session.mode_changed")] +[JsonDerivedType(typeof(SessionModeNoticeDeliveredEvent), "session.mode_notice_delivered")] [JsonDerivedType(typeof(SessionModelChangeEvent), "session.model_change")] [JsonDerivedType(typeof(SessionPermissionsChangedEvent), "session.permissions_changed")] [JsonDerivedType(typeof(SessionPlanChangedEvent), "session.plan_changed")] @@ -378,6 +379,19 @@ public sealed partial class SessionModeChangedEvent : SessionEvent public required SessionModeChangedData Data { get; set; } } +/// Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +/// Represents the session.mode_notice_delivered event. +public sealed partial class SessionModeNoticeDeliveredEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.mode_notice_delivered"; + + /// The session.mode_notice_delivered event payload. + [JsonPropertyName("data")] + public required SessionModeNoticeDeliveredData Data { get; set; } +} + /// Session limits update details. Null clears the limits. /// Represents the session.session_limits_changed event. public sealed partial class SessionSessionLimitsChangedEvent : SessionEvent @@ -1546,7 +1560,7 @@ public sealed partial class SessionAutoModeResolvedEvent : SessionEvent public required SessionAutoModeResolvedData Data { get; set; } } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. /// Represents the session.managed_settings_resolved event. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsResolvedEvent : SessionEvent @@ -2346,6 +2360,19 @@ public sealed partial class SessionModeChangedData public required SessionMode PreviousMode { get; set; } } +/// Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +public sealed partial class SessionModeNoticeDeliveredData +{ + /// Model-visible transition notice persisted for a mid-turn delivery. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// Mode established by the delivered transition notice. + [JsonPropertyName("mode")] + public required SessionMode Mode { get; set; } +} + /// Session limits update details. Null clears the limits. public sealed partial class SessionSessionLimitsChangedData { @@ -5410,7 +5437,7 @@ public sealed partial class SessionAutoModeResolvedData public bool? StickyOverride { get; set; } } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsResolvedData { @@ -5440,6 +5467,11 @@ public sealed partial class SessionManagedSettingsResolvedData [JsonPropertyName("permissionsAllowIntersected")] public bool? PermissionsAllowIntersected { get; set; } + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("policyHelperManaged")] + public bool? PolicyHelperManaged { get; set; } + /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("sandboxEnabledByUndeterminedPolicy")] @@ -5454,7 +5486,7 @@ public sealed partial class SessionManagedSettingsResolvedData [JsonPropertyName("settings")] public JsonElement? Settings { get; set; } - /// Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. [JsonPropertyName("source")] public required ManagedSettingsResolvedSource Source { get; set; } } @@ -7068,7 +7100,7 @@ public sealed partial class FusionAttribution [Experimental(Diagnostics.Experimental)] public sealed partial class AssistantMessageReasoningBlocks { - /// Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + /// Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("blocks")] public JsonElement[]? Blocks { get; set; } @@ -14833,7 +14865,10 @@ public ManagedSettingsResolvedSource(string value) /// Only session-local SDK-host injection contributed. public static ManagedSettingsResolvedSource Client { get; } = new("client"); - /// More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + /// A policy helper registered by device or server policy contributed. Device registration takes priority when present. + public static ManagedSettingsResolvedSource PolicyHelper { get; } = new("policyHelper"); + + /// More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. public static ManagedSettingsResolvedSource Mixed { get; } = new("mixed"); /// No managed policy is in force (no channel contributed). @@ -15861,6 +15896,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionMcpServersLoadedEvent))] [JsonSerializable(typeof(SessionModeChangedData))] [JsonSerializable(typeof(SessionModeChangedEvent))] +[JsonSerializable(typeof(SessionModeNoticeDeliveredData))] +[JsonSerializable(typeof(SessionModeNoticeDeliveredEvent))] [JsonSerializable(typeof(SessionModelChangeData))] [JsonSerializable(typeof(SessionModelChangeEvent))] [JsonSerializable(typeof(SessionPermissionsChangedData))] diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 67121c30ed..1b2458590d 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -1604,7 +1604,8 @@ type CatalogSearchRequest struct { Kinds []CatalogCandidateKind `json:"kinds,omitzero"` // Maximum number of candidates to return. Defaults to 10 when omitted. Limit *int32 `json:"limit,omitempty"` - // Free-text search query. Never written to logs or telemetry. + // Free-text search query. Persisted as tool input for session continuity, but omitted from + // telemetry. Query string `json:"query"` } @@ -1737,6 +1738,10 @@ type CatalogNetworkFailureError struct { Message string `json:"message"` // Categorised failure, low cardinality so it can be aggregated without carrying a URL. Reason CatalogNetworkFailureReason `json:"reason"` + // Bounded cooldown in seconds before another catalog request should be attempted, when the + // authority supplied a numeric Retry-After value or the runtime applied its documented + // fallback. + RetryAfterSeconds *int32 `json:"retryAfterSeconds,omitempty"` // HTTP status code, when the failure was a rejected response. StatusCode *int32 `json:"statusCode,omitempty"` } @@ -3564,6 +3569,18 @@ func (FactoryRunFailureFactoryLimitReached) Type() FactoryRunFailureType { return FactoryRunFailureTypeFactoryLimitReached } +// The extension that owns the factory disconnected while the run was executing, so the host +// halted it. The run's journaled subagent results are preserved so a resume can reuse them. +type FactoryRunFailureFactoryProviderDisconnected struct { + // Factory run identifier. + RunID string `json:"runId"` +} + +func (FactoryRunFailureFactoryProviderDisconnected) factoryRunFailure() {} +func (FactoryRunFailureFactoryProviderDisconnected) Type() FactoryRunFailureType { + return FactoryRunFailureTypeFactoryProviderDisconnected +} + type FactoryRunFailureFactoryResumeDeclined struct { // Human-readable reason the resume did not proceed. Reason string `json:"reason"` @@ -3609,9 +3626,12 @@ type FactoryRunRequest struct { // Experimental: FactoryRunResult is part of an experimental API and may change or be // removed. type FactoryRunResult struct { + // One-based execution attempt represented by this envelope. Absent before the first attempt + // starts or when returned by an older runtime. + Attempt *int64 `json:"attempt,omitempty"` // Error message for an errored run. Error *string `json:"error,omitempty"` - // Machine-readable failure details for an errored run. + // Machine-readable failure details for a halted or errored run. Failure FactoryRunFailure `json:"failure,omitempty"` // Reason for a halted or cancelled run. Reason *string `json:"reason,omitempty"` @@ -6880,6 +6900,10 @@ type ModelApplyStartupOverlayRequest struct { DeferredResume *bool `json:"deferredResume,omitempty"` // Model required by device-managed policy, when configured. DeviceManagedModel *string `json:"deviceManagedModel,omitempty"` + // Startup default model from the enterprise policy helper, when configured. Weakest of the + // managed sources: it applies only when neither device nor server policy names a model, and + // an explicit user selection still wins. + PolicyHelperModel *string `json:"policyHelperModel,omitempty"` // Context tier selected by repository settings, when configured. RepoContextTier *string `json:"repoContextTier,omitempty"` // Model selected by repository settings, when configured. @@ -6921,6 +6945,12 @@ type ModelBillingPromo struct { // Human-readable promotion message. Does not include the expiry timestamp; consumers may // format endsAt and append it when present. Message *string `json:"message,omitempty"` + // Whether the service asked hosts to give this promotion a prominent surface, such as a + // dedicated banner, in addition to listing it with the model. `true` requests that surface + // and `false` asks for the model list only. Absent means the service expressed no + // preference — for example a response that predates the field — so hosts should apply their + // own default rather than read it as `false`. + ShowBanner *bool `json:"showBanner,omitempty"` } // Token-level pricing information for this model @@ -13703,6 +13733,8 @@ func (SlashCommandAgentPromptResult) Kind() SlashCommandInvocationResultKind { type SlashCommandCompletedResult struct { // Optional user-facing message describing the completed command Message *string `json:"message,omitempty"` + // Optional target session mode applied without submitting an agent prompt + Mode *SessionMode `json:"mode,omitempty"` // True when the invocation mutated user runtime settings; consumers caching settings should // refresh RuntimeSettingsChanged *bool `json:"runtimeSettingsChanged,omitempty"` @@ -16122,14 +16154,20 @@ const ( CatalogNetworkFailureReasonConnectionRefused CatalogNetworkFailureReason = "connection-refused" // The authority's name could not be resolved. CatalogNetworkFailureReasonDns CatalogNetworkFailureReason = "dns" - // The authority returned a status the runtime treats as a failure. + // The authority returned another status the runtime treats as a failure. CatalogNetworkFailureReasonHTTPStatus CatalogNetworkFailureReason = "http-status" // No network is available, so nothing was attempted. CatalogNetworkFailureReasonOffline CatalogNetworkFailureReason = "offline" + // The configured proxy returned 407 and requires authentication. + CatalogNetworkFailureReasonProxyAuthenticationRequired CatalogNetworkFailureReason = "proxy-authentication-required" + // The authority rate-limited requests and supplied or implied a bounded cooldown. + CatalogNetworkFailureReasonRateLimited CatalogNetworkFailureReason = "rate-limited" // A redirect was refused by the runtime's redirect policy. CatalogNetworkFailureReasonRedirectRejected CatalogNetworkFailureReason = "redirect-rejected" // The response exceeded the permitted size. CatalogNetworkFailureReasonResponseTooLarge CatalogNetworkFailureReason = "response-too-large" + // The authority returned a transient 5xx response. + CatalogNetworkFailureReasonServiceUnavailable CatalogNetworkFailureReason = "service-unavailable" // The request exceeded its time budget. CatalogNetworkFailureReasonTimeout CatalogNetworkFailureReason = "timeout" // The TLS handshake or certificate validation failed. @@ -16592,6 +16630,7 @@ const ( FactoryRunFailureTypeFactoryAccountingIncomplete FactoryRunFailureType = "factory_accounting_incomplete" FactoryRunFailureTypeFactoryDurableFailure FactoryRunFailureType = "factory_durable_failure" FactoryRunFailureTypeFactoryLimitReached FactoryRunFailureType = "factory_limit_reached" + FactoryRunFailureTypeFactoryProviderDisconnected FactoryRunFailureType = "factory_provider_disconnected" FactoryRunFailureTypeFactoryResumeDeclined FactoryRunFailureType = "factory_resume_declined" ) @@ -27461,6 +27500,9 @@ func (a *InternalModelAPI) ApplyStartupOverlay(ctx context.Context, params *Mode if params.DeviceManagedModel != nil { req["deviceManagedModel"] = *params.DeviceManagedModel } + if params.PolicyHelperModel != nil { + req["policyHelperModel"] = *params.PolicyHelperModel + } if params.RepoContextTier != nil { req["repoContextTier"] = *params.RepoContextTier } diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 78788660f5..197e5b5437 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -1606,6 +1606,12 @@ func unmarshalFactoryRunFailure(data []byte) (FactoryRunFailure, error) { return nil, err } return &d, nil + case FactoryRunFailureTypeFactoryProviderDisconnected: + var d FactoryRunFailureFactoryProviderDisconnected + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case FactoryRunFailureTypeFactoryResumeDeclined: var d FactoryRunFailureFactoryResumeDeclined if err := json.Unmarshal(data, &d); err != nil { @@ -1661,6 +1667,17 @@ func (r FactoryRunFailureFactoryLimitReached) MarshalJSON() ([]byte, error) { }) } +func (r FactoryRunFailureFactoryProviderDisconnected) MarshalJSON() ([]byte, error) { + type alias FactoryRunFailureFactoryProviderDisconnected + return json.Marshal(struct { + Type FactoryRunFailureType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r FactoryRunFailureFactoryResumeDeclined) MarshalJSON() ([]byte, error) { type alias FactoryRunFailureFactoryResumeDeclined return json.Marshal(struct { @@ -1698,6 +1715,7 @@ func (r *FactoryRunTerminal) UnmarshalJSON(data []byte) error { func (r *FactoryRunResult) UnmarshalJSON(data []byte) error { type rawFactoryRunResult struct { + Attempt *int64 `json:"attempt,omitempty"` Error *string `json:"error,omitempty"` Failure json.RawMessage `json:"failure,omitempty"` Reason *string `json:"reason,omitempty"` @@ -1710,6 +1728,7 @@ func (r *FactoryRunResult) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &raw); err != nil { return err } + r.Attempt = raw.Attempt r.Error = raw.Error if raw.Failure != nil { value, err := unmarshalFactoryRunFailure(raw.Failure) diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index 4c03a42c00..05e372402b 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -575,6 +575,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionModeNoticeDelivered: + var d SessionModeNoticeDeliveredData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionPermissionsChanged: var d SessionPermissionsChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index acea96f5b9..0f93b52f18 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -183,6 +183,7 @@ const ( SessionEventTypeSessionMCPServerStatusChanged SessionEventType = "session.mcp_server_status_changed" SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed" SessionEventTypeSessionModelChange SessionEventType = "session.model_change" + SessionEventTypeSessionModeNoticeDelivered SessionEventType = "session.mode_notice_delivered" // Experimental: SessionEventTypeSessionPermissionsChanged identifies an experimental event // that may change or be removed. SessionEventTypeSessionPermissionsChanged SessionEventType = "session.permissions_changed" @@ -768,7 +769,7 @@ func (*PendingMessagesModifiedData) Type() SessionEventType { return SessionEventTypePendingMessagesModified } -// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. // Experimental: SessionManagedSettingsResolvedData is part of an experimental API and may change or be removed. type SessionManagedSettingsResolvedData struct { // Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. @@ -783,13 +784,15 @@ type SessionManagedSettingsResolvedData struct { ManagedKeys []string `json:"managedKeys"` // Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. PermissionsAllowIntersected *bool `json:"permissionsAllowIntersected,omitempty"` + // Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + PolicyHelperManaged *bool `json:"policyHelperManaged,omitempty"` // Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. SandboxEnabledByUndeterminedPolicy *bool `json:"sandboxEnabledByUndeterminedPolicy,omitempty"` // Whether the server (account/org) managed-settings layer was present ServerManaged bool `json:"serverManaged"` // The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. Settings any `json:"settings,omitempty"` - // Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + // Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. Source ManagedSettingsResolvedSource `json:"source"` } @@ -1960,6 +1963,19 @@ type CommandQueuedData struct { func (*CommandQueuedData) sessionEventData() {} func (*CommandQueuedData) Type() SessionEventType { return SessionEventTypeCommandQueued } +// Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +type SessionModeNoticeDeliveredData struct { + // Model-visible transition notice persisted for a mid-turn delivery + Content *string `json:"content,omitempty"` + // Mode established by the delivered transition notice + Mode SessionMode `json:"mode"` +} + +func (*SessionModeNoticeDeliveredData) sessionEventData() {} +func (*SessionModeNoticeDeliveredData) Type() SessionEventType { + return SessionEventTypeSessionModeNoticeDelivered +} + // Registered command dispatch request routed to the owning client type CommandExecuteData struct { // Raw argument string after the command name @@ -2787,7 +2803,7 @@ func (*SessionWorkspaceFileChangedData) Type() SessionEventType { // Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping // Experimental: AssistantMessageReasoningBlocks is part of an experimental API and may change or be removed. type AssistantMessageReasoningBlocks struct { - // Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + // Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. Blocks []any `json:"blocks,omitzero"` // Model provider that produced these reasoning blocks. Provider string `json:"provider"` @@ -5284,10 +5300,12 @@ const ( ManagedSettingsResolvedSourceClient ManagedSettingsResolvedSource = "client" // Only the device MDM/plist/registry/file channel contributed. ManagedSettingsResolvedSourceDevice ManagedSettingsResolvedSource = "device" - // More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + // More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. ManagedSettingsResolvedSourceMixed ManagedSettingsResolvedSource = "mixed" // No managed policy is in force (no channel contributed). ManagedSettingsResolvedSourceNone ManagedSettingsResolvedSource = "none" + // A policy helper registered by device or server policy contributed. Device registration takes priority when present. + ManagedSettingsResolvedSourcePolicyHelper ManagedSettingsResolvedSource = "policyHelper" // Only the server/account channel contributed. ManagedSettingsResolvedSourceServer ManagedSettingsResolvedSource = "server" ) diff --git a/go/zsession_events.go b/go/zsession_events.go index bdd9c918f8..360f1a7588 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -294,6 +294,7 @@ type ( SessionMode = rpc.SessionMode SessionModeChangedData = rpc.SessionModeChangedData SessionModelChangeData = rpc.SessionModelChangeData + SessionModeNoticeDeliveredData = rpc.SessionModeNoticeDeliveredData SessionPermissionsChangedData = rpc.SessionPermissionsChangedData SessionPlanChangedData = rpc.SessionPlanChangedData SessionRemoteSteerableChangedData = rpc.SessionRemoteSteerableChangedData @@ -551,6 +552,7 @@ const ( ManagedSettingsResolvedSourceDevice = rpc.ManagedSettingsResolvedSourceDevice ManagedSettingsResolvedSourceMixed = rpc.ManagedSettingsResolvedSourceMixed ManagedSettingsResolvedSourceNone = rpc.ManagedSettingsResolvedSourceNone + ManagedSettingsResolvedSourcePolicyHelper = rpc.ManagedSettingsResolvedSourcePolicyHelper ManagedSettingsResolvedSourceServer = rpc.ManagedSettingsResolvedSourceServer MCPHeadersRefreshCompletedOutcomeHeaders = rpc.MCPHeadersRefreshCompletedOutcomeHeaders MCPHeadersRefreshCompletedOutcomeNone = rpc.MCPHeadersRefreshCompletedOutcomeNone @@ -757,6 +759,7 @@ const ( SessionEventTypeSessionMCPServerStatusChanged = rpc.SessionEventTypeSessionMCPServerStatusChanged SessionEventTypeSessionModeChanged = rpc.SessionEventTypeSessionModeChanged SessionEventTypeSessionModelChange = rpc.SessionEventTypeSessionModelChange + SessionEventTypeSessionModeNoticeDelivered = rpc.SessionEventTypeSessionModeNoticeDelivered SessionEventTypeSessionPermissionsChanged = rpc.SessionEventTypeSessionPermissionsChanged SessionEventTypeSessionPlanChanged = rpc.SessionEventTypeSessionPlanChanged SessionEventTypeSessionRemoteSteerableChanged = rpc.SessionEventTypeSessionRemoteSteerableChanged diff --git a/java/pom.xml b/java/pom.xml index 91a97aadff..30d9d582d6 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -63,7 +63,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.83-0 + ^1.0.83-1 true diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index fd177ca1e8..2c4712269a 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.83-0", + "@github/copilot": "^1.0.83-1", "json-schema": "^0.4.0", "tsx": "^4.23.12" } @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-0.tgz", - "integrity": "sha512-Nv4IsqsveMgghwaBhgvSBZyIyvsqNBZTqnbVnv69+9+Suyq20vJcv6aB74UcJ7VPCMxIGJJUaJkugEtkMNv6wA==", + "version": "1.0.83-1", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-1.tgz", + "integrity": "sha512-7rhgbgSx7IgfFYXrTx2/2+qowv6MZjC6BN0mhba37RuGQxsca9Zk2ucAeIX3GbQTp91AhvnN0aHCxabX36xADQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-0", - "@github/copilot-darwin-x64": "1.0.83-0", - "@github/copilot-linux-arm64": "1.0.83-0", - "@github/copilot-linux-x64": "1.0.83-0", - "@github/copilot-linuxmusl-arm64": "1.0.83-0", - "@github/copilot-linuxmusl-x64": "1.0.83-0", - "@github/copilot-win32-arm64": "1.0.83-0", - "@github/copilot-win32-x64": "1.0.83-0" + "@github/copilot-darwin-arm64": "1.0.83-1", + "@github/copilot-darwin-x64": "1.0.83-1", + "@github/copilot-linux-arm64": "1.0.83-1", + "@github/copilot-linux-x64": "1.0.83-1", + "@github/copilot-linuxmusl-arm64": "1.0.83-1", + "@github/copilot-linuxmusl-x64": "1.0.83-1", + "@github/copilot-win32-arm64": "1.0.83-1", + "@github/copilot-win32-x64": "1.0.83-1" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-0.tgz", - "integrity": "sha512-0KQjKS9vd4QGxLAbFJcvyv/zsC5kivrtDe0UZhHt/43nUGqoS61DFcsM596/kg75vNE6c9J4gmZ5fUPYef+0hw==", + "version": "1.0.83-1", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-1.tgz", + "integrity": "sha512-gxYJEd4yoIBIQHRmJw6ZegSkR6VuNm7AJVdAdARcU0SS68VZjtZFT29luThAp75Pxp37S5w+3A/jQNtxGyHizw==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-0.tgz", - "integrity": "sha512-fiyW+hy4c8AI7ONxN623f9cmJGRpbqTztc0jSVXc9z9WwzcWi39X0nxUprRM2l2Dq6YQ3guPCqGl/g1T5bQfQg==", + "version": "1.0.83-1", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-1.tgz", + "integrity": "sha512-PL63+Ib1RH+Bpu2UYJ9E8tmD4NZD/YlhD25F1Q9Rb86AdgLEoRYlxjNxXXF9P9dMmJ/u5zm0rUtWIRxv/FRbPg==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-0.tgz", - "integrity": "sha512-RWbRU+KgEmtAdKp1GQVTqfdwg4Ti/OVmgZGkXq4lMYj3wnBBQcayFpSLHg5ShzDSS0RglD4b8Z27NjPrm7bXxA==", + "version": "1.0.83-1", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-1.tgz", + "integrity": "sha512-7c4RVEuXQu9zepAWoXwkizpESztUbWUR1hLPl+o+19L+D8fXm+5XQRH5E22Ia7P7/xHW3Ml2o2x5cXFq0k7/7w==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-0.tgz", - "integrity": "sha512-5COXUNT+jDfkeyqrymZMvhTogkBYUXt+wuRwKrK6ol5vaw5SoDP1DYbI2hIEfoUj4g7XTHLUCD1s3lw8eicqUA==", + "version": "1.0.83-1", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-1.tgz", + "integrity": "sha512-Be0zUVysLYV66pnOPhMxpShQ8Ox5V07qcsSOoNzG7iwpzkfZuf70U9CHk/fnI//mWwmZR+caPUfVM9BpwrdNnA==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-0.tgz", - "integrity": "sha512-7sYf364iz6s97ClviBRQusTKz3S3TgoKniyYv8+aRi5f5w6TL8NTPnGX1bXMeU0VZmk5VKQTlxVRO2yA4uFwpg==", + "version": "1.0.83-1", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-1.tgz", + "integrity": "sha512-PZNP8vZZJU8yIc3dNjGUBUCFPNjqZs8FPZLDxvL6XdHM7Ep4cLBa+QdbRsw+nZONIaG0GtQf26eqk5ymYYpUPA==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-0.tgz", - "integrity": "sha512-jze/f6Yd3Y83kxUa88kXUiwHlZmHDwAqudswdHT6f6q+K1ZEELFGEzbB6Ku4i0L8M6wHXO1EF/zaiSFWQaM4Tw==", + "version": "1.0.83-1", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-1.tgz", + "integrity": "sha512-uKMhGtg5K07nA5/pxDSq8S5F/I+sMgVhUXg90Y4BI9trOidgxpXpCVyh4TeRNj2ENNugPvATdfGc6eFPsjmlgQ==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-0.tgz", - "integrity": "sha512-93jln98UAJpslMQ7n+wAmCpoOWGEV5lXxV/DaEajySvYrCU33D2yj7d9kl8X2CgaUVBas6sNWSWKtqy+rKJxXQ==", + "version": "1.0.83-1", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-1.tgz", + "integrity": "sha512-vMxHTmv3SotBuNI4LKcIFckd+n0OvhTik27qjsqOnVqFpvmHDghSsa8YPxySyxUQIE35z5pJaivVBK3dfEAY7w==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-0.tgz", - "integrity": "sha512-+4Htk3CixO1qcOtYegjn33/8bSDdx8QXDpgVBak2D4Y5hzBWPO5IuQoICwvjaW5VOIW+I7Q62RK2pupSjxB38Q==", + "version": "1.0.83-1", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-1.tgz", + "integrity": "sha512-2l0VVTazW3/+p94ZQ5sAPCj1iH7KWxJAYh4yXWfvpsB/1JEEmyVgpLei1MeaMFVdtp/hi9bGprZ9+zWNlHh5hQ==", "cpu": [ "x64" ], diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 8089a79133..2e2e1f7e19 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.83-0", + "@github/copilot": "^1.0.83-1", "json-schema": "^0.4.0", "tsx": "^4.23.12" } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java index d2ad87f7c4..ee9c5fd485 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java @@ -24,7 +24,7 @@ public record AssistantMessageReasoningBlocks( /** Model provider that produced these reasoning blocks. */ @JsonProperty("provider") String provider, - /** Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. */ + /** Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. */ @JsonProperty("blocks") List blocks ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java index 32386f898a..90ce9de668 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java @@ -22,6 +22,8 @@ public enum ManagedSettingsResolvedSource { DEVICE("device"), /** The {@code client} variant. */ CLIENT("client"), + /** The {@code policyHelper} variant. */ + POLICYHELPER("policyHelper"), /** The {@code mixed} variant. */ MIXED("mixed"), /** The {@code none} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java index e22561b914..bf5dc65769 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -39,6 +39,7 @@ @JsonSubTypes.Type(value = SessionWarningEvent.class, name = "session.warning"), @JsonSubTypes.Type(value = SessionModelChangeEvent.class, name = "session.model_change"), @JsonSubTypes.Type(value = SessionModeChangedEvent.class, name = "session.mode_changed"), + @JsonSubTypes.Type(value = SessionModeNoticeDeliveredEvent.class, name = "session.mode_notice_delivered"), @JsonSubTypes.Type(value = SessionSessionLimitsChangedEvent.class, name = "session.session_limits_changed"), @JsonSubTypes.Type(value = SessionPermissionsChangedEvent.class, name = "session.permissions_changed"), @JsonSubTypes.Type(value = SessionPlanChangedEvent.class, name = "session.plan_changed"), @@ -172,6 +173,7 @@ public abstract sealed class SessionEvent permits SessionWarningEvent, SessionModelChangeEvent, SessionModeChangedEvent, + SessionModeNoticeDeliveredEvent, SessionSessionLimitsChangedEvent, SessionPermissionsChangedEvent, SessionPlanChangedEvent, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java index ac3763248a..e5428d26db 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -35,7 +35,7 @@ public final class SessionManagedSettingsResolvedEvent extends SessionEvent { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record SessionManagedSettingsResolvedEventData( - /** Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. */ + /** Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. */ @JsonProperty("source") ManagedSettingsResolvedSource source, /** Whether the server (account/org) managed-settings layer was present */ @JsonProperty("serverManaged") Boolean serverManaged, @@ -43,6 +43,8 @@ public record SessionManagedSettingsResolvedEventData( @JsonProperty("deviceManaged") Boolean deviceManaged, /** Whether a session-local permissions layer injected by the SDK host was present */ @JsonProperty("clientManaged") Boolean clientManaged, + /** Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. */ + @JsonProperty("policyHelperManaged") Boolean policyHelperManaged, /** Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. */ @JsonProperty("failClosed") Boolean failClosed, /** Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.java new file mode 100644 index 0000000000..9e24109f9a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.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 "session.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionModeNoticeDeliveredEvent extends SessionEvent { + + @Override + public String getType() { return "session.mode_notice_delivered"; } + + @JsonProperty("data") + private SessionModeNoticeDeliveredEventData data; + + public SessionModeNoticeDeliveredEventData getData() { return data; } + public void setData(SessionModeNoticeDeliveredEventData data) { this.data = data; } + + /** Data payload for {@link SessionModeNoticeDeliveredEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionModeNoticeDeliveredEventData( + /** Mode established by the delivered transition notice */ + @JsonProperty("mode") SessionMode mode, + /** Model-visible transition notice persisted for a mid-turn delivery */ + @JsonProperty("content") String content + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java index d4afb32c3d..01d92e908a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java @@ -36,6 +36,10 @@ public final class CatalogNetworkFailureError extends CatalogSearchResult { @JsonProperty("statusCode") private Long statusCode; + /** Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. */ + @JsonProperty("retryAfterSeconds") + private Long retryAfterSeconds; + /** Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ @JsonProperty("message") private String message; @@ -46,6 +50,9 @@ public final class CatalogNetworkFailureError extends CatalogSearchResult { public Long getStatusCode() { return statusCode; } public void setStatusCode(Long statusCode) { this.statusCode = statusCode; } + public Long getRetryAfterSeconds() { return retryAfterSeconds; } + public void setRetryAfterSeconds(Long retryAfterSeconds) { this.retryAfterSeconds = retryAfterSeconds; } + public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java index c28d1cc4a7..ac821b062d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java @@ -26,6 +26,12 @@ public enum CatalogNetworkFailureReason { TLS("tls"), /** The {@code connection-refused} variant. */ CONNECTION_REFUSED("connection-refused"), + /** The {@code proxy-authentication-required} variant. */ + PROXY_AUTHENTICATION_REQUIRED("proxy-authentication-required"), + /** The {@code rate-limited} variant. */ + RATE_LIMITED("rate-limited"), + /** The {@code service-unavailable} variant. */ + SERVICE_UNAVAILABLE("service-unavailable"), /** The {@code http-status} variant. */ HTTP_STATUS("http-status"), /** The {@code response-too-large} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java index ee5e703621..68b90a19c9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java @@ -27,7 +27,7 @@ public record CatalogSearchParams( /** Protocol version and capabilities the caller requires. */ @JsonProperty("contract") CatalogClientContract contract, - /** Free-text search query. Never written to logs or telemetry. */ + /** Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. */ @JsonProperty("query") String query, /** Maximum number of candidates to return. Defaults to 10 when omitted. */ @JsonProperty("limit") Long limit, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java index bb28f40887..71ee3c49e6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java @@ -23,13 +23,15 @@ public record FactoryRunResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java index 087ca1c15a..d44087dcff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java @@ -28,6 +28,8 @@ public record ModelBillingPromo( /** UTC ISO 8601 timestamp marking when the promotion ends. Optional: an open-ended promotion omits this field. When present, the API only surfaces a promo whose expiry parses and is in the future, so consumers should treat a past value as expired. */ @JsonProperty("endsAt") String endsAt, /** Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. */ - @JsonProperty("message") String message + @JsonProperty("message") String message, + /** Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. */ + @JsonProperty("showBanner") Boolean showBanner ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java index 0cb66280c0..c9f9de2dcc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java @@ -26,13 +26,15 @@ public record SessionFactoryCancelResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java index 6742faf03e..2d6a5f52a9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java @@ -26,13 +26,15 @@ public record SessionFactoryGetRunResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java index 71b12f94f1..1a9dee5926 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java @@ -26,13 +26,15 @@ public record SessionFactoryRunFromToolResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java index 46083f2284..d8ce481895 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java @@ -26,13 +26,15 @@ public record SessionFactoryRunResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java index a1d25ae7ec..dfb593fd21 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java @@ -30,6 +30,8 @@ public record SessionModelApplyStartupOverlayParams( @JsonProperty("deviceManagedModel") String deviceManagedModel, /** Model required by server-managed policy, when configured. */ @JsonProperty("serverManagedModel") String serverManagedModel, + /** Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. */ + @JsonProperty("policyHelperModel") String policyHelperModel, /** Model selected by repository settings, when configured. */ @JsonProperty("repoModel") String repoModel, /** Reasoning effort selected by repository settings, when configured. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java index b2a1970a36..c457181435 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java @@ -32,6 +32,10 @@ public final class SlashCommandCompletedResult extends SlashCommandInvocationRes @JsonProperty("message") private String message; + /** Optional target session mode applied without submitting an agent prompt */ + @JsonProperty("mode") + private SessionMode mode; + /** True when the invocation mutated user runtime settings; consumers caching settings should refresh */ @JsonProperty("runtimeSettingsChanged") private Boolean runtimeSettingsChanged; @@ -39,6 +43,9 @@ public final class SlashCommandCompletedResult extends SlashCommandInvocationRes public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } + public SessionMode getMode() { return mode; } + public void setMode(SessionMode mode) { this.mode = mode; } + public Boolean getRuntimeSettingsChanged() { return runtimeSettingsChanged; } public void setRuntimeSettingsChanged(Boolean runtimeSettingsChanged) { this.runtimeSettingsChanged = runtimeSettingsChanged; } } diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 602089d012..638f535582 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -816,7 +816,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 promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount"); + var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount", true); var billing = new ModelBilling(1.0, null, null, promo); var modelItem = new Model("gpt-5", "GPT-5", capabilities, policy, billing, null, null, null, null, null, null, null, null); @@ -834,6 +834,7 @@ void modelsListResult_nested() { assertEquals(Double.valueOf(25.0), result.models().get(0).billing().promo().discountPercent()); assertEquals("2026-08-01T00:00:00Z", result.models().get(0).billing().promo().endsAt()); assertEquals("Summer discount", result.models().get(0).billing().promo().message()); + assertTrue(result.models().get(0).billing().promo().showBanner()); } @Test diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 6480e85e29..39bb781f7b 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-0", + "@github/copilot": "^1.0.83-1", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -658,8 +658,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-0", - "integrity": "sha512-Nv4IsqsveMgghwaBhgvSBZyIyvsqNBZTqnbVnv69+9+Suyq20vJcv6aB74UcJ7VPCMxIGJJUaJkugEtkMNv6wA==", + "version": "1.0.83-1", + "integrity": "sha512-7rhgbgSx7IgfFYXrTx2/2+qowv6MZjC6BN0mhba37RuGQxsca9Zk2ucAeIX3GbQTp91AhvnN0aHCxabX36xADQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -668,19 +668,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-0", - "@github/copilot-darwin-x64": "1.0.83-0", - "@github/copilot-linux-arm64": "1.0.83-0", - "@github/copilot-linux-x64": "1.0.83-0", - "@github/copilot-linuxmusl-arm64": "1.0.83-0", - "@github/copilot-linuxmusl-x64": "1.0.83-0", - "@github/copilot-win32-arm64": "1.0.83-0", - "@github/copilot-win32-x64": "1.0.83-0" + "@github/copilot-darwin-arm64": "1.0.83-1", + "@github/copilot-darwin-x64": "1.0.83-1", + "@github/copilot-linux-arm64": "1.0.83-1", + "@github/copilot-linux-x64": "1.0.83-1", + "@github/copilot-linuxmusl-arm64": "1.0.83-1", + "@github/copilot-linuxmusl-x64": "1.0.83-1", + "@github/copilot-win32-arm64": "1.0.83-1", + "@github/copilot-win32-x64": "1.0.83-1" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-0KQjKS9vd4QGxLAbFJcvyv/zsC5kivrtDe0UZhHt/43nUGqoS61DFcsM596/kg75vNE6c9J4gmZ5fUPYef+0hw==", + "version": "1.0.83-1", + "integrity": "sha512-gxYJEd4yoIBIQHRmJw6ZegSkR6VuNm7AJVdAdARcU0SS68VZjtZFT29luThAp75Pxp37S5w+3A/jQNtxGyHizw==", "cpu": [ "arm64" ], @@ -694,8 +694,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-0", - "integrity": "sha512-fiyW+hy4c8AI7ONxN623f9cmJGRpbqTztc0jSVXc9z9WwzcWi39X0nxUprRM2l2Dq6YQ3guPCqGl/g1T5bQfQg==", + "version": "1.0.83-1", + "integrity": "sha512-PL63+Ib1RH+Bpu2UYJ9E8tmD4NZD/YlhD25F1Q9Rb86AdgLEoRYlxjNxXXF9P9dMmJ/u5zm0rUtWIRxv/FRbPg==", "cpu": [ "x64" ], @@ -709,8 +709,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-RWbRU+KgEmtAdKp1GQVTqfdwg4Ti/OVmgZGkXq4lMYj3wnBBQcayFpSLHg5ShzDSS0RglD4b8Z27NjPrm7bXxA==", + "version": "1.0.83-1", + "integrity": "sha512-7c4RVEuXQu9zepAWoXwkizpESztUbWUR1hLPl+o+19L+D8fXm+5XQRH5E22Ia7P7/xHW3Ml2o2x5cXFq0k7/7w==", "cpu": [ "arm64" ], @@ -724,8 +724,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-0", - "integrity": "sha512-5COXUNT+jDfkeyqrymZMvhTogkBYUXt+wuRwKrK6ol5vaw5SoDP1DYbI2hIEfoUj4g7XTHLUCD1s3lw8eicqUA==", + "version": "1.0.83-1", + "integrity": "sha512-Be0zUVysLYV66pnOPhMxpShQ8Ox5V07qcsSOoNzG7iwpzkfZuf70U9CHk/fnI//mWwmZR+caPUfVM9BpwrdNnA==", "cpu": [ "x64" ], @@ -739,8 +739,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-7sYf364iz6s97ClviBRQusTKz3S3TgoKniyYv8+aRi5f5w6TL8NTPnGX1bXMeU0VZmk5VKQTlxVRO2yA4uFwpg==", + "version": "1.0.83-1", + "integrity": "sha512-PZNP8vZZJU8yIc3dNjGUBUCFPNjqZs8FPZLDxvL6XdHM7Ep4cLBa+QdbRsw+nZONIaG0GtQf26eqk5ymYYpUPA==", "cpu": [ "arm64" ], @@ -754,8 +754,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-0", - "integrity": "sha512-jze/f6Yd3Y83kxUa88kXUiwHlZmHDwAqudswdHT6f6q+K1ZEELFGEzbB6Ku4i0L8M6wHXO1EF/zaiSFWQaM4Tw==", + "version": "1.0.83-1", + "integrity": "sha512-uKMhGtg5K07nA5/pxDSq8S5F/I+sMgVhUXg90Y4BI9trOidgxpXpCVyh4TeRNj2ENNugPvATdfGc6eFPsjmlgQ==", "cpu": [ "x64" ], @@ -769,8 +769,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-93jln98UAJpslMQ7n+wAmCpoOWGEV5lXxV/DaEajySvYrCU33D2yj7d9kl8X2CgaUVBas6sNWSWKtqy+rKJxXQ==", + "version": "1.0.83-1", + "integrity": "sha512-vMxHTmv3SotBuNI4LKcIFckd+n0OvhTik27qjsqOnVqFpvmHDghSsa8YPxySyxUQIE35z5pJaivVBK3dfEAY7w==", "cpu": [ "arm64" ], @@ -784,8 +784,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-0", - "integrity": "sha512-+4Htk3CixO1qcOtYegjn33/8bSDdx8QXDpgVBak2D4Y5hzBWPO5IuQoICwvjaW5VOIW+I7Q62RK2pupSjxB38Q==", + "version": "1.0.83-1", + "integrity": "sha512-2l0VVTazW3/+p94ZQ5sAPCj1iH7KWxJAYh4yXWfvpsB/1JEEmyVgpLei1MeaMFVdtp/hi9bGprZ9+zWNlHh5hQ==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index e1744c4fdc..da1892a794 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-0", + "@github/copilot": "^1.0.83-1", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 0ef1a077b2..bbb9cea89d 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-0", + "@github/copilot": "^1.0.83-1", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 32a002dd01..812c6b2607 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -552,7 +552,13 @@ export type CatalogNetworkFailureReason = | "tls" /** The connection was refused or reset. */ | "connection-refused" - /** The authority returned a status the runtime treats as a failure. */ + /** The configured proxy returned 407 and requires authentication. */ + | "proxy-authentication-required" + /** The authority rate-limited requests and supplied or implied a bounded cooldown. */ + | "rate-limited" + /** The authority returned a transient 5xx response. */ + | "service-unavailable" + /** The authority returned another status the runtime treats as a failure. */ | "http-status" /** The response exceeded the permitted size. */ | "response-too-large" @@ -1127,6 +1133,16 @@ export type FactoryRunFailure = * Factory failure variant discriminator. */ type: "factory_accounting_incomplete"; + } + | { + /** + * Factory run identifier. + */ + runId: string; + /** + * Factory failure variant discriminator. + */ + type: "factory_provider_disconnected"; }; /** * Cumulative resource ceiling that stopped a factory run. @@ -5834,6 +5850,10 @@ export interface CatalogNetworkFailureError { * HTTP status code, when the failure was a rejected response. */ statusCode?: number; + /** + * Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + */ + retryAfterSeconds?: number; /** * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ @@ -5885,7 +5905,7 @@ export interface CatalogPolicyRejectedError { export interface CatalogSearchRequest { contract: CatalogClientContract; /** - * Free-text search query. Never written to logs or telemetry. + * Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. */ query: string; /** @@ -8156,6 +8176,10 @@ export interface FactoryRunResult { * Factory run identifier. */ runId: string; + /** + * One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + */ + attempt?: number; status: FactoryRunStatus; /** * Completed factory result. @@ -12356,6 +12380,10 @@ export interface ModelBillingPromo { * Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. */ message?: string; + /** + * Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + */ + showBanner?: boolean; } /** * Service-published warning text that hosts should display when presenting a model. @@ -12403,6 +12431,10 @@ export interface ModelApplyStartupOverlayRequest { * Model required by server-managed policy, when configured. */ serverManagedModel?: string; + /** + * Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. + */ + policyHelperModel?: string; /** * Model selected by repository settings, when configured. */ @@ -20287,6 +20319,7 @@ export interface SlashCommandCompletedResult { * Optional user-facing message describing the completed command */ message?: string; + mode?: SessionMode; /** * True when the invocation mutated user runtime settings; consumers caching settings should refresh */ diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index c7a610abef..a7049dd9fc 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -24,6 +24,7 @@ export type SessionEvent = | WarningEvent | ModelChangeEvent | ModeChangedEvent + | ModeNoticeDeliveredEvent | SessionLimitsChangedEvent | PermissionsChangedEvent | PlanChangedEvent @@ -947,7 +948,9 @@ export type ManagedSettingsResolvedSource = | "device" /** Only session-local SDK-host injection contributed. */ | "client" - /** More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. */ + /** A policy helper registered by device or server policy contributed. Device registration takes priority when present. */ + | "policyHelper" + /** More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. */ | "mixed" /** No managed policy is in force (no channel contributed). */ | "none"; @@ -1901,6 +1904,46 @@ export interface ModeChangedData { newMode: SessionMode; previousMode: SessionMode; } +/** + * Session event "session.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. + */ +export interface ModeNoticeDeliveredEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: ModeNoticeDeliveredData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * 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 "session.mode_notice_delivered". + */ + type: "session.mode_notice_delivered"; +} +/** + * Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. + */ +export interface ModeNoticeDeliveredData { + /** + * Model-visible transition notice persisted for a mid-turn delivery + */ + content?: string; + mode: SessionMode; +} /** * Session event "session.session_limits_changed". Session limits update details. Null clears the limits. */ @@ -4815,7 +4858,7 @@ export interface FusionAttribution { /** @experimental */ export interface AssistantMessageReasoningBlocks { /** - * Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + * Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. */ blocks?: JsonValue[]; /** @@ -10068,7 +10111,7 @@ export interface AutoModeResolvedData { stickyOverride?: boolean; } /** - * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. */ /** @experimental */ export interface ManagedSettingsResolvedEvent { @@ -10099,7 +10142,7 @@ export interface ManagedSettingsResolvedEvent { type: "session.managed_settings_resolved"; } /** - * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. */ /** @experimental */ export interface ManagedSettingsResolvedData { @@ -10127,6 +10170,10 @@ export interface ManagedSettingsResolvedData { * Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. */ permissionsAllowIntersected?: boolean; + /** + * Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + */ + policyHelperManaged?: boolean; /** * Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. */ diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 1340d726be..7b629d5c18 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -1331,8 +1331,11 @@ class CatalogNetworkFailureReason(Enum): DNS = "dns" HTTP_STATUS = "http-status" OFFLINE = "offline" + PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required" + RATE_LIMITED = "rate-limited" REDIRECT_REJECTED = "redirect-rejected" RESPONSE_TOO_LARGE = "response-too-large" + SERVICE_UNAVAILABLE = "service-unavailable" TIMEOUT = "timeout" TLS = "tls" @@ -1427,12 +1430,15 @@ class CatalogSearchResultReason(Enum): NO_CREDENTIAL = "no-credential" OFFLINE = "offline" PLANNING_UNAVAILABLE = "planning-unavailable" + PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required" PROXY_REJECTED = "proxy-rejected" + RATE_LIMITED = "rate-limited" REDIRECT_REJECTED = "redirect-rejected" REDIRECT_TO_BLOCKED_ADDRESS = "redirect-to-blocked-address" RESPONSE_TOO_LARGE = "response-too-large" SCHEMA_VIOLATION = "schema-violation" SEARCH_UNAVAILABLE = "search-unavailable" + SERVICE_UNAVAILABLE = "service-unavailable" SIZE_LIMIT_EXCEEDED = "size-limit-exceeded" TIMEOUT = "timeout" TLS = "tls" @@ -3355,6 +3361,7 @@ class FactoryRunFailureType(Enum): FACTORY_ACCOUNTING_INCOMPLETE = "factory_accounting_incomplete" FACTORY_DURABLE_FAILURE = "factory_durable_failure" FACTORY_LIMIT_REACHED = "factory_limit_reached" + FACTORY_PROVIDER_DISCONNECTED = "factory_provider_disconnected" FACTORY_RESUME_DECLINED = "factory_resume_declined" # Experimental: this type is part of an experimental API and may change or be removed. @@ -5736,7 +5743,9 @@ class MCPPlanInstallResultReason(Enum): OFFLINE = "offline" PLANNING_UNAVAILABLE = "planning-unavailable" POLICY_FORBIDS = "policy-forbids" + PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required" PROXY_REJECTED = "proxy-rejected" + RATE_LIMITED = "rate-limited" REDIRECT_REJECTED = "redirect-rejected" REDIRECT_TO_BLOCKED_ADDRESS = "redirect-to-blocked-address" REMOTE_ENUMERATION_UNAVAILABLE = "remote-enumeration-unavailable" @@ -5744,6 +5753,7 @@ class MCPPlanInstallResultReason(Enum): RESPONSE_TOO_LARGE = "response-too-large" SCHEMA_VIOLATION = "schema-violation" SEARCH_UNAVAILABLE = "search-unavailable" + SERVICE_UNAVAILABLE = "service-unavailable" SIZE_LIMIT_EXCEEDED = "size-limit-exceeded" STALE = "stale" TIMEOUT = "timeout" @@ -6596,6 +6606,13 @@ class ModelBillingPromo: """Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. """ + show_banner: bool | None = None + """Whether the service asked hosts to give this promotion a prominent surface, such as a + dedicated banner, in addition to listing it with the model. `true` requests that surface + and `false` asks for the model list only. Absent means the service expressed no + preference — for example a response that predates the field — so hosts should apply their + own default rather than read it as `false`. + """ @staticmethod def from_dict(obj: Any) -> 'ModelBillingPromo': @@ -6604,7 +6621,8 @@ def from_dict(obj: Any) -> 'ModelBillingPromo': ends_at = from_union([from_str, from_none], obj.get("endsAt")) id = from_union([from_str, from_none], obj.get("id")) message = from_union([from_str, from_none], obj.get("message")) - return ModelBillingPromo(discount_percent, ends_at, id, message) + show_banner = from_union([from_bool, from_none], obj.get("showBanner")) + return ModelBillingPromo(discount_percent, ends_at, id, message, show_banner) def to_dict(self) -> dict: result: dict = {} @@ -6616,6 +6634,8 @@ def to_dict(self) -> dict: result["id"] = from_union([from_str, from_none], self.id) if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) + if self.show_banner is not None: + result["showBanner"] = from_union([from_bool, from_none], self.show_banner) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -15112,8 +15132,9 @@ class CatalogSearchRequest: """Protocol version and capabilities the caller requires.""" query: str - """Free-text search query. Never written to logs or telemetry.""" - + """Free-text search query. Persisted as tool input for session continuity, but omitted from + telemetry. + """ kinds: list[CatalogCandidateKind] | None = None """Restrict results to these candidate kinds. When omitted, every kind the runtime supports is searched. @@ -15402,6 +15423,11 @@ class CatalogNetworkFailureError: reason: CatalogNetworkFailureReason """Categorised failure, low cardinality so it can be aggregated without carrying a URL.""" + retry_after_seconds: int | None = None + """Bounded cooldown in seconds before another catalog request should be attempted, when the + authority supplied a numeric Retry-After value or the runtime applied its documented + fallback. + """ status_code: int | None = None """HTTP status code, when the failure was a rejected response.""" @@ -15410,14 +15436,17 @@ def from_dict(obj: Any) -> 'CatalogNetworkFailureError': assert isinstance(obj, dict) message = from_str(obj.get("message")) reason = CatalogNetworkFailureReason(obj.get("reason")) + retry_after_seconds = from_union([from_int, from_none], obj.get("retryAfterSeconds")) status_code = from_union([from_int, from_none], obj.get("statusCode")) - return CatalogNetworkFailureError(message, reason, status_code) + return CatalogNetworkFailureError(message, reason, retry_after_seconds, status_code) def to_dict(self) -> dict: result: dict = {} result["kind"] = self.kind result["message"] = from_str(self.message) result["reason"] = to_enum(CatalogNetworkFailureReason, self.reason) + if self.retry_after_seconds is not None: + result["retryAfterSeconds"] = from_union([from_int, from_none], self.retry_after_seconds) if self.status_code is not None: result["statusCode"] = from_union([from_int, from_none], self.status_code) return result @@ -16969,9 +16998,12 @@ class FactoryRunFailure: Machine-readable factory run failure. - Machine-readable failure details for an errored run. + Machine-readable failure details for a halted or errored run. The run stopped because its usage accounting could not be completed. + + The extension that owns the factory disconnected while the run was executing, so the host + halted it. The run's journaled subagent results are preserved so a resume can reuse them. """ run_id: str """Factory run identifier. @@ -23737,6 +23769,9 @@ class SlashCommandCompletedResult: message: str | None = None """Optional user-facing message describing the completed command""" + mode: SessionMode | None = None + """Optional target session mode applied without submitting an agent prompt""" + runtime_settings_changed: bool | None = None """True when the invocation mutated user runtime settings; consumers caching settings should refresh @@ -23746,14 +23781,17 @@ class SlashCommandCompletedResult: def from_dict(obj: Any) -> 'SlashCommandCompletedResult': assert isinstance(obj, dict) message = from_union([from_str, from_none], obj.get("message")) + mode = from_union([SessionMode, from_none], obj.get("mode")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandCompletedResult(message, runtime_settings_changed) + return SlashCommandCompletedResult(message, mode, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} result["kind"] = self.kind if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(SessionMode, x), from_none], self.mode) if self.runtime_settings_changed is not None: result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) return result @@ -26040,11 +26078,15 @@ class FactoryRunResult: status: FactoryRunStatus """Current or terminal factory run status.""" + attempt: int | None = None + """One-based execution attempt represented by this envelope. Absent before the first attempt + starts or when returned by an older runtime. + """ error: str | None = None """Error message for an errored run.""" failure: FactoryRunFailure | None = None - """Machine-readable failure details for an errored run.""" + """Machine-readable failure details for a halted or errored run.""" reason: str | None = None """Reason for a halted or cancelled run.""" @@ -26060,17 +26102,20 @@ def from_dict(obj: Any) -> 'FactoryRunResult': assert isinstance(obj, dict) run_id = from_str(obj.get("runId")) status = FactoryRunStatus(obj.get("status")) + attempt = from_union([from_int, from_none], obj.get("attempt")) error = from_union([from_str, from_none], obj.get("error")) failure = from_union([FactoryRunFailure.from_dict, from_none], obj.get("failure")) reason = from_union([from_str, from_none], obj.get("reason")) result = obj.get("result") snapshot = obj.get("snapshot") - return FactoryRunResult(run_id, status, error, failure, reason, result, snapshot) + return FactoryRunResult(run_id, status, attempt, error, failure, reason, result, snapshot) def to_dict(self) -> dict: result: dict = {} result["runId"] = from_str(self.run_id) result["status"] = to_enum(FactoryRunStatus, self.status) + if self.attempt is not None: + result["attempt"] = from_union([from_int, from_none], self.attempt) if self.error is not None: result["error"] = from_union([from_str, from_none], self.error) if self.failure is not None: @@ -33964,6 +34009,11 @@ class ModelApplyStartupOverlayRequest: device_managed_model: str | None = None """Model required by device-managed policy, when configured.""" + policy_helper_model: str | None = None + """Startup default model from the enterprise policy helper, when configured. Weakest of the + managed sources: it applies only when neither device nor server policy names a model, and + an explicit user selection still wins. + """ repo_context_tier: str | None = None """Context tier selected by repository settings, when configured.""" @@ -33982,11 +34032,12 @@ def from_dict(obj: Any) -> 'ModelApplyStartupOverlayRequest': cli_model = from_union([from_str, from_none], obj.get("cliModel")) deferred_resume = from_union([from_bool, from_none], obj.get("deferredResume")) device_managed_model = from_union([from_str, from_none], obj.get("deviceManagedModel")) + policy_helper_model = from_union([from_str, from_none], obj.get("policyHelperModel")) repo_context_tier = from_union([from_str, from_none], obj.get("repoContextTier")) repo_model = from_union([from_str, from_none], obj.get("repoModel")) repo_reasoning_effort = from_union([from_str, from_none], obj.get("repoReasoningEffort")) server_managed_model = from_union([from_str, from_none], obj.get("serverManagedModel")) - return ModelApplyStartupOverlayRequest(cli_model, deferred_resume, device_managed_model, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model) + return ModelApplyStartupOverlayRequest(cli_model, deferred_resume, device_managed_model, policy_helper_model, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model) def to_dict(self) -> dict: result: dict = {} @@ -33996,6 +34047,8 @@ def to_dict(self) -> dict: result["deferredResume"] = from_union([from_bool, from_none], self.deferred_resume) if self.device_managed_model is not None: result["deviceManagedModel"] = from_union([from_str, from_none], self.device_managed_model) + if self.policy_helper_model is not None: + result["policyHelperModel"] = from_union([from_str, from_none], self.policy_helper_model) if self.repo_context_tier is not None: result["repoContextTier"] = from_union([from_str, from_none], self.repo_context_tier) if self.repo_model is not None: diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index fd41b6c6c6..20b3df4be4 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -137,6 +137,7 @@ class SessionEventType(Enum): SESSION_WARNING = "session.warning" SESSION_MODEL_CHANGE = "session.model_change" SESSION_MODE_CHANGED = "session.mode_changed" + SESSION_MODE_NOTICE_DELIVERED = "session.mode_notice_delivered" SESSION_SESSION_LIMITS_CHANGED = "session.session_limits_changed" # Experimental: this event is part of an experimental API and may change or be removed. SESSION_PERMISSIONS_CHANGED = "session.permissions_changed" @@ -1992,7 +1993,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionManagedSettingsResolvedData: - "Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes." + "Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes." bypass_permissions_disabled: bool device_managed: bool fail_closed: bool @@ -2001,6 +2002,7 @@ class SessionManagedSettingsResolvedData: source: ManagedSettingsResolvedSource client_managed: bool | None = None permissions_allow_intersected: bool | None = None + policy_helper_managed: bool | None = None sandbox_enabled_by_undetermined_policy: bool | None = None settings: Any = None @@ -2015,6 +2017,7 @@ def from_dict(obj: Any) -> "SessionManagedSettingsResolvedData": source = parse_enum(ManagedSettingsResolvedSource, obj.get("source")) client_managed = from_union([from_none, from_bool], obj.get("clientManaged")) permissions_allow_intersected = from_union([from_none, from_bool], obj.get("permissionsAllowIntersected")) + policy_helper_managed = from_union([from_none, from_bool], obj.get("policyHelperManaged")) sandbox_enabled_by_undetermined_policy = from_union([from_none, from_bool], obj.get("sandboxEnabledByUndeterminedPolicy")) settings = obj.get("settings") return SessionManagedSettingsResolvedData( @@ -2026,6 +2029,7 @@ def from_dict(obj: Any) -> "SessionManagedSettingsResolvedData": source=source, client_managed=client_managed, permissions_allow_intersected=permissions_allow_intersected, + policy_helper_managed=policy_helper_managed, sandbox_enabled_by_undetermined_policy=sandbox_enabled_by_undetermined_policy, settings=settings, ) @@ -2042,6 +2046,8 @@ def to_dict(self) -> dict: result["clientManaged"] = from_union([from_none, from_bool], self.client_managed) if self.permissions_allow_intersected is not None: result["permissionsAllowIntersected"] = from_union([from_none, from_bool], self.permissions_allow_intersected) + if self.policy_helper_managed is not None: + result["policyHelperManaged"] = from_union([from_none, from_bool], self.policy_helper_managed) if self.sandbox_enabled_by_undetermined_policy is not None: result["sandboxEnabledByUndeterminedPolicy"] = from_union([from_none, from_bool], self.sandbox_enabled_by_undetermined_policy) if self.settings is not None: @@ -8137,6 +8143,30 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionModeNoticeDeliveredData: + "Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume." + mode: SessionMode + content: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionModeNoticeDeliveredData": + assert isinstance(obj, dict) + mode = parse_enum(SessionMode, obj.get("mode")) + content = from_union([from_none, from_str], obj.get("content")) + return SessionModeNoticeDeliveredData( + mode=mode, + content=content, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["mode"] = to_enum(SessionMode, self.mode) + if self.content is not None: + result["content"] = from_union([from_none, from_str], self.content) + return result + + @dataclass class SessionModelChangeData: "Model change details including previous and new model identifiers" @@ -11920,7 +11950,9 @@ class ManagedSettingsResolvedSource(Enum): DEVICE = "device" # Only session-local SDK-host injection contributed. CLIENT = "client" - # More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + # A policy helper registered by device or server policy contributed. Device registration takes priority when present. + POLICY_HELPER = "policyHelper" + # More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. MIXED = "mixed" # No managed policy is in force (no channel contributed). NONE = "none" @@ -12334,7 +12366,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | 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 | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -12374,6 +12406,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_WARNING: data = SessionWarningData.from_dict(data_obj) case SessionEventType.SESSION_MODEL_CHANGE: data = SessionModelChangeData.from_dict(data_obj) case SessionEventType.SESSION_MODE_CHANGED: data = SessionModeChangedData.from_dict(data_obj) + case SessionEventType.SESSION_MODE_NOTICE_DELIVERED: data = SessionModeNoticeDeliveredData.from_dict(data_obj) case SessionEventType.SESSION_SESSION_LIMITS_CHANGED: data = SessionSessionLimitsChangedData.from_dict(data_obj) case SessionEventType.SESSION_PERMISSIONS_CHANGED: data = SessionPermissionsChangedData.from_dict(data_obj) case SessionEventType.SESSION_PLAN_CHANGED: data = SessionPlanChangedData.from_dict(data_obj) @@ -12796,6 +12829,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SessionMcpServersLoadedData", "SessionMode", "SessionModeChangedData", + "SessionModeNoticeDeliveredData", "SessionModelChangeData", "SessionPermissionsChangedData", "SessionPlanChangedData", diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 00cad43217..a42b9994fc 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -181,6 +181,7 @@ def test_managed_settings_client_provenance_round_trips(self): "server", "device", "client", + "policyHelper", "mixed", "none", ] diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 701ae61403..b1d9896f3e 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -3365,6 +3365,9 @@ pub struct CatalogNetworkFailureError { pub message: String, /// Categorised failure, low cardinality so it can be aggregated without carrying a URL. pub reason: CatalogNetworkFailureReason, + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + #[serde(skip_serializing_if = "Option::is_none")] + pub retry_after_seconds: Option, /// HTTP status code, when the failure was a rejected response. #[serde(skip_serializing_if = "Option::is_none")] pub status_code: Option, @@ -3427,7 +3430,7 @@ pub struct CatalogSearchRequest { /// Maximum number of candidates to return. Defaults to 10 when omitted. #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, - /// Free-text search query. Never written to logs or telemetry. + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. pub query: String, } @@ -5621,10 +5624,13 @@ pub struct FactoryResumeRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FactoryRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -9886,6 +9892,9 @@ pub struct ModelBillingPromo { /// Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + #[serde(skip_serializing_if = "Option::is_none")] + pub show_banner: Option, } /// Long context tier pricing (available for models with extended context windows) @@ -10212,6 +10221,9 @@ pub struct ModelApplyStartupOverlayRequest { /// Model required by device-managed policy, when configured. #[serde(skip_serializing_if = "Option::is_none")] pub device_managed_model: Option, + /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_helper_model: Option, /// Context tier selected by repository settings, when configured. #[serde(skip_serializing_if = "Option::is_none")] pub repo_context_tier: Option, @@ -18823,6 +18835,9 @@ pub struct SlashCommandCompletedResult { /// Optional user-facing message describing the completed command #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// Optional target session mode applied without submitting an agent prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, /// True when the invocation mutated user runtime settings; consumers caching settings should refresh #[serde(skip_serializing_if = "Option::is_none")] pub runtime_settings_changed: Option, @@ -22388,10 +22403,13 @@ pub struct SessionCanvasActionInvokeResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22437,10 +22455,13 @@ pub struct SessionFactoryResumeResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryRunFromToolResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22486,10 +22507,13 @@ pub struct SessionFactoryResumeFromToolResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryGetRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22627,10 +22651,13 @@ pub struct SessionFactoryGetRunProgressResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryCancelResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -28378,7 +28405,16 @@ pub enum CatalogNetworkFailureReason { /// The connection was refused or reset. #[serde(rename = "connection-refused")] ConnectionRefused, - /// The authority returned a status the runtime treats as a failure. + /// The configured proxy returned 407 and requires authentication. + #[serde(rename = "proxy-authentication-required")] + ProxyAuthenticationRequired, + /// The authority rate-limited requests and supplied or implied a bounded cooldown. + #[serde(rename = "rate-limited")] + RateLimited, + /// The authority returned a transient 5xx response. + #[serde(rename = "service-unavailable")] + ServiceUnavailable, + /// The authority returned another status the runtime treats as a failure. #[serde(rename = "http-status")] HttpStatus, /// The response exceeded the permitted size. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 4b04b03444..8c48ac62ca 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -39,6 +39,8 @@ pub enum SessionEventType { SessionModelChange, #[serde(rename = "session.mode_changed")] SessionModeChanged, + #[serde(rename = "session.mode_notice_delivered")] + SessionModeNoticeDelivered, #[serde(rename = "session.session_limits_changed")] SessionSessionLimitsChanged, /// @@ -465,6 +467,8 @@ pub enum SessionEventData { SessionModelChange(SessionModelChangeData), #[serde(rename = "session.mode_changed")] SessionModeChanged(SessionModeChangedData), + #[serde(rename = "session.mode_notice_delivered")] + SessionModeNoticeDelivered(SessionModeNoticeDeliveredData), #[serde(rename = "session.session_limits_changed")] SessionSessionLimitsChanged(SessionSessionLimitsChangedData), /// @@ -1235,6 +1239,17 @@ pub struct SessionModeChangedData { pub previous_mode: SessionMode, } +/// Session event "session.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionModeNoticeDeliveredData { + /// Model-visible transition notice persisted for a mid-turn delivery + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + /// Mode established by the delivered transition notice + pub mode: SessionMode, +} + /// Session event "session.session_limits_changed". Session limits update details. Null clears the limits. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -2499,7 +2514,7 @@ pub struct FusionAttribution { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AssistantMessageReasoningBlocks { - /// Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + /// Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. #[serde(skip_serializing_if = "Option::is_none")] pub blocks: Option>, /// Model provider that produced these reasoning blocks. @@ -5711,7 +5726,7 @@ pub struct SessionAutoModeResolvedData { pub sticky_override: Option, } -/// Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. /// ///
/// @@ -5736,6 +5751,9 @@ pub struct SessionManagedSettingsResolvedData { /// Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. #[serde(skip_serializing_if = "Option::is_none")] pub permissions_allow_intersected: Option, + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_helper_managed: Option, /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_enabled_by_undetermined_policy: Option, @@ -5744,7 +5762,7 @@ pub struct SessionManagedSettingsResolvedData { /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. #[serde(skip_serializing_if = "Option::is_none")] pub settings: Option, - /// Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. pub source: ManagedSettingsResolvedSource, } @@ -8158,7 +8176,10 @@ pub enum ManagedSettingsResolvedSource { /// Only session-local SDK-host injection contributed. #[serde(rename = "client")] Client, - /// More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + /// A policy helper registered by device or server policy contributed. Device registration takes priority when present. + #[serde(rename = "policyHelper")] + PolicyHelper, + /// More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. #[serde(rename = "mixed")] Mixed, /// No managed policy is in force (no channel contributed). diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index a83d73d0a9..274e70f675 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.83-0", + "@github/copilot": "^1.0.83-1", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -472,8 +472,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-0", - "integrity": "sha512-Nv4IsqsveMgghwaBhgvSBZyIyvsqNBZTqnbVnv69+9+Suyq20vJcv6aB74UcJ7VPCMxIGJJUaJkugEtkMNv6wA==", + "version": "1.0.83-1", + "integrity": "sha512-7rhgbgSx7IgfFYXrTx2/2+qowv6MZjC6BN0mhba37RuGQxsca9Zk2ucAeIX3GbQTp91AhvnN0aHCxabX36xADQ==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -483,19 +483,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-0", - "@github/copilot-darwin-x64": "1.0.83-0", - "@github/copilot-linux-arm64": "1.0.83-0", - "@github/copilot-linux-x64": "1.0.83-0", - "@github/copilot-linuxmusl-arm64": "1.0.83-0", - "@github/copilot-linuxmusl-x64": "1.0.83-0", - "@github/copilot-win32-arm64": "1.0.83-0", - "@github/copilot-win32-x64": "1.0.83-0" + "@github/copilot-darwin-arm64": "1.0.83-1", + "@github/copilot-darwin-x64": "1.0.83-1", + "@github/copilot-linux-arm64": "1.0.83-1", + "@github/copilot-linux-x64": "1.0.83-1", + "@github/copilot-linuxmusl-arm64": "1.0.83-1", + "@github/copilot-linuxmusl-x64": "1.0.83-1", + "@github/copilot-win32-arm64": "1.0.83-1", + "@github/copilot-win32-x64": "1.0.83-1" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-0KQjKS9vd4QGxLAbFJcvyv/zsC5kivrtDe0UZhHt/43nUGqoS61DFcsM596/kg75vNE6c9J4gmZ5fUPYef+0hw==", + "version": "1.0.83-1", + "integrity": "sha512-gxYJEd4yoIBIQHRmJw6ZegSkR6VuNm7AJVdAdARcU0SS68VZjtZFT29luThAp75Pxp37S5w+3A/jQNtxGyHizw==", "cpu": [ "arm64" ], @@ -510,8 +510,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-0", - "integrity": "sha512-fiyW+hy4c8AI7ONxN623f9cmJGRpbqTztc0jSVXc9z9WwzcWi39X0nxUprRM2l2Dq6YQ3guPCqGl/g1T5bQfQg==", + "version": "1.0.83-1", + "integrity": "sha512-PL63+Ib1RH+Bpu2UYJ9E8tmD4NZD/YlhD25F1Q9Rb86AdgLEoRYlxjNxXXF9P9dMmJ/u5zm0rUtWIRxv/FRbPg==", "cpu": [ "x64" ], @@ -526,8 +526,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-RWbRU+KgEmtAdKp1GQVTqfdwg4Ti/OVmgZGkXq4lMYj3wnBBQcayFpSLHg5ShzDSS0RglD4b8Z27NjPrm7bXxA==", + "version": "1.0.83-1", + "integrity": "sha512-7c4RVEuXQu9zepAWoXwkizpESztUbWUR1hLPl+o+19L+D8fXm+5XQRH5E22Ia7P7/xHW3Ml2o2x5cXFq0k7/7w==", "cpu": [ "arm64" ], @@ -542,8 +542,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-0", - "integrity": "sha512-5COXUNT+jDfkeyqrymZMvhTogkBYUXt+wuRwKrK6ol5vaw5SoDP1DYbI2hIEfoUj4g7XTHLUCD1s3lw8eicqUA==", + "version": "1.0.83-1", + "integrity": "sha512-Be0zUVysLYV66pnOPhMxpShQ8Ox5V07qcsSOoNzG7iwpzkfZuf70U9CHk/fnI//mWwmZR+caPUfVM9BpwrdNnA==", "cpu": [ "x64" ], @@ -558,8 +558,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-7sYf364iz6s97ClviBRQusTKz3S3TgoKniyYv8+aRi5f5w6TL8NTPnGX1bXMeU0VZmk5VKQTlxVRO2yA4uFwpg==", + "version": "1.0.83-1", + "integrity": "sha512-PZNP8vZZJU8yIc3dNjGUBUCFPNjqZs8FPZLDxvL6XdHM7Ep4cLBa+QdbRsw+nZONIaG0GtQf26eqk5ymYYpUPA==", "cpu": [ "arm64" ], @@ -574,8 +574,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-0", - "integrity": "sha512-jze/f6Yd3Y83kxUa88kXUiwHlZmHDwAqudswdHT6f6q+K1ZEELFGEzbB6Ku4i0L8M6wHXO1EF/zaiSFWQaM4Tw==", + "version": "1.0.83-1", + "integrity": "sha512-uKMhGtg5K07nA5/pxDSq8S5F/I+sMgVhUXg90Y4BI9trOidgxpXpCVyh4TeRNj2ENNugPvATdfGc6eFPsjmlgQ==", "cpu": [ "x64" ], @@ -590,8 +590,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-93jln98UAJpslMQ7n+wAmCpoOWGEV5lXxV/DaEajySvYrCU33D2yj7d9kl8X2CgaUVBas6sNWSWKtqy+rKJxXQ==", + "version": "1.0.83-1", + "integrity": "sha512-vMxHTmv3SotBuNI4LKcIFckd+n0OvhTik27qjsqOnVqFpvmHDghSsa8YPxySyxUQIE35z5pJaivVBK3dfEAY7w==", "cpu": [ "arm64" ], @@ -606,8 +606,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-0", - "integrity": "sha512-+4Htk3CixO1qcOtYegjn33/8bSDdx8QXDpgVBak2D4Y5hzBWPO5IuQoICwvjaW5VOIW+I7Q62RK2pupSjxB38Q==", + "version": "1.0.83-1", + "integrity": "sha512-2l0VVTazW3/+p94ZQ5sAPCj1iH7KWxJAYh4yXWfvpsB/1JEEmyVgpLei1MeaMFVdtp/hi9bGprZ9+zWNlHh5hQ==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index c2081214ab..e293910938 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.83-0", + "@github/copilot": "^1.0.83-1", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", From 66479cbee1419039bf474a5d8a785a25d20a0204 Mon Sep 17 00:00:00 2001 From: Kerry Liu Date: Wed, 2 Sep 2026 04:51:06 +0000 Subject: [PATCH 22/60] Add clientInfo connect option across all six SDKs (#2464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add clientInfo connect option across all six SDKs Let a host declare its identity (editorName, editorVersion, extensionName, extensionVersion) on the server.connect handshake so the telemetry the runtime emits on that connection is attributed to the host editor and its Copilot extension instead of the runtime's own build. Each SDK adds a hand-written public option that maps onto the generated clientInfo wire type (generated files untouched). It is forwarded only when set and non-empty; an all-empty identity is dropped from the handshake. Adds a Client info docs page, features-index entry, CHANGELOG note, and per-language tests covering verbatim forwarding, omission when unset, and omission of empty fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Fix lint/format: drop quoted Python annotation, prettier-wrap Node test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Align client-info doc copy with sibling feature guides Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Address review: fix Java field regression, enforce empty-field contract - Java: restore one-declaration-per-line (Spotless) in CopilotClientOptions - Enforce the documented 'drop empty fields, omit all-empty identity' contract in Node, Python, Java (@JsonInclude NON_EMPTY + blank-aware isEmpty), and Rust, matching Go/.NET; add empty-string tests in each - Node: add clientInfoToWire helper; fix test option type (CopilotClientOptions) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Format: wrap client_info wire comprehension for ruff line length Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Rewrite client_info wire mapping with literal keys for ty Avoids TypedDict non-literal-key subscript so no type: ignore is needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Apply spotless formatting to Java clientInfo changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Fix Rust from_transport call after main merge added process_tree arg The merge of main added a `process_tree: Option` parameter to `from_transport` (after `child`). The test-support helper `from_streams_with_client_info` still passed 13 args, breaking the build on all Rust lanes. Pass `None` for the new parameter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Address Copilot review feedback on clientInfo - Rust: make ClientInfo #[non_exhaustive] with new()/with_* builder methods so future identity fields aren't a breaking change; update the integration tests and docs snippets to the builder instead of struct literals. - .NET: tighten the unset-omission test to assert clientInfo is absent (TryGetProperty == false) rather than accepting an explicit null. - Go: add an all-empty &ClientInfo{} subtest covering the distinct branch that must omit the top-level clientInfo. - CHANGELOG: reword the literal `clientInfo` to language-neutral client info with each SDK's idiomatic name, so non-Node users aren't pointed at a nonexistent option. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Use TypeScript tab label in client-info doc The docs-style whitelist lists `Node.js` and `TypeScript` as separate labels; the combined `Node.js / TypeScript` isn't permitted. Recent docs commits are migrating the legacy combined form to `TypeScript`, so match that direction here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Fix Java clientInfo wire leak and add .NET empty-field tests Opus 5 review found two issues: - Java: Jackson auto-detected `ClientInfo.isEmpty()` as a boolean property and serialized `{"empty":false,...}` on the connect handshake, diverging from the other five SDKs. Annotate `isEmpty()` with `@JsonIgnore` and tighten two tests to assert the exact clientInfo field set (size 4 / 1), which catches the regression. - .NET: add coverage for the empty-string drop path (EditorVersion = "") and the all-empty omit path (Connect_Omits_All_Empty_ClientInfo), mirroring the other SDKs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * [temp] Bump gh-aw compiler to v0.87.10 to fix SDK Consistency Review model The SDK Consistency Review workflow fails on every branch because its baked-in default model `claude-sonnet-4.6` is no longer available for the `agentic-workflows` integrator (Copilot API returns HTTP 400). Recompiling with gh-aw v0.87.10 changes the Copilot default from `claude-sonnet-4.6` to `auto`, letting the CLI pick a supported model instead of hardcoding one. Recompiled only sdk-consistency-review.md via `gh aw compile`. Side effects of the version bump: compiler v0.83.1 -> v0.87.10, copilot engine 1.0.73 -> 1.0.80, actions-lock setup bumped to v0.87.10, and .gitattributes dropped `merge=ours` on lock files. Temporary/experimental — evaluate whether to keep here or split to its own PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d * Revert "[temp] Bump gh-aw compiler to v0.87.10 to fix SDK Consistency Review model" This reverts commit f2f2ff9596563ddc62398364e31390a29dcd0b15. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3dca665a-ee1d-4df5-99b6-8711728f238d --- CHANGELOG.md | 4 + docs/features/README.md | 1 + docs/features/client-info.md | 239 ++++++++++++++++++ dotnet/src/Client.cs | 44 +++- dotnet/src/Types.cs | 43 ++++ dotnet/test/Unit/CloneTests.cs | 8 + dotnet/test/Unit/GitHubTelemetryTests.cs | 83 ++++++ go/client.go | 9 +- go/client_test.go | 100 ++++++++ go/types.go | 56 ++++ .../com/github/copilot/CopilotClient.java | 7 + .../com/github/copilot/rpc/ClientInfo.java | 144 +++++++++++ .../copilot/rpc/CopilotClientOptions.java | 32 +++ .../github/copilot/GitHubTelemetryTest.java | 90 +++++++ nodejs/src/client.ts | 29 +++ nodejs/src/index.ts | 1 + nodejs/src/types.ts | 41 +++ nodejs/test/client.test.ts | 74 ++++++ python/copilot/__init__.py | 2 + python/copilot/client.py | 54 ++++ python/test_client.py | 99 ++++++++ rust/src/lib.rs | 159 +++++++++++- rust/tests/protocol_version_test.rs | 150 +++++++++++ 23 files changed, 1464 insertions(+), 5 deletions(-) create mode 100644 docs/features/client-info.md create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0184a553db..54e0c94d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu ## [Unreleased] +### Feature: declare host identity with client info + +Client options now accept optional client info (editor name and version, extension name and version) across all six SDKs, exposed idiomatically per language (`clientInfo` in Node.js, `client_info` in Python and Rust, `ClientInfo` in Go and .NET, `setClientInfo` in Java). When set, the SDK forwards it on the `server.connect` handshake so the telemetry the runtime emits on the connection is attributed to the host editor and its Copilot extension instead of the runtime's own build. All fields are optional, and leaving client info unset keeps the runtime's default attribution. See [Client info](./docs/features/client-info.md). + ### Feature: Node Agent Factories pagination and run notifications The experimental Node.js Agent Factories convenience API now supports paginated run history. Existing `session.factory.listRuns()` calls still return the runs array, while calls with `afterSeq`, `beforeSeq`, or `limit` return the full page with cursor and truncation metadata. diff --git a/docs/features/README.md b/docs/features/README.md index f97140b784..6350958720 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -20,6 +20,7 @@ These guides cover the capabilities you can add to your Copilot SDK application. | [Image Input](./image-input.md) | Send images to sessions as attachments | | [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) | | [Usage and Billing](./usage-and-billing.md) | Read token counts, context-window utilization, AI credit cost, and account quota | +| [Client info](./client-info.md) | Declare the host editor and extension so runtime telemetry is attributed to your surface | | [Steering & Queueing](./steering-and-queueing.md) | Control message delivery—immediate steering vs. sequential queueing | | [Context Clearing](./context-management.md) | Replace conversation context safely with terminal tools | | [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage | diff --git a/docs/features/client-info.md b/docs/features/client-info.md new file mode 100644 index 0000000000..48ada2d13a --- /dev/null +++ b/docs/features/client-info.md @@ -0,0 +1,239 @@ +# Client info + +Client info identifies the editor or extension that hosts your Copilot SDK application. Set the optional `clientInfo` client option to attribute runtime telemetry for that connection to your host instead of the runtime's own build. + +## When to set client info + +Set client info when you embed the SDK in an editor, an extension, or another application with its own identity. + +Leave client info unset for scripts, one-off tools, and back-end jobs that do not represent a distinct host. The runtime then keeps its default attribution. + +Client info has four optional string fields. Set the fields you know and omit the rest. The SDK includes client info in the `server.connect` handshake only when at least one field has a non-empty value. + +| Field | Example | Meaning | +|---|---|---| +| `editorName` | `"vscode"` | Name of the host editor | +| `editorVersion` | `"1.124.2"` | Version of the host editor | +| `extensionName` | `"copilot-chat"` | Name of the Copilot extension within the host | +| `extensionVersion` | `"0.54.0"` | Version of the Copilot extension within the host | + +The SDK sends client info once when it establishes the connection. The identity applies for the lifetime of that connection. + +## Configure client info + +Pass client info when you create the client: + +
+TypeScript + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +async function main() { + const client = new CopilotClient({ + clientInfo: { + editorName: "JetBrains-IU", + editorVersion: "2026.1", + extensionName: "copilot-intellij", + extensionVersion: "1.5.0", + }, + }); + + await client.start(); +} + +main(); +``` + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + clientInfo: { + editorName: "JetBrains-IU", + editorVersion: "2026.1", + extensionName: "copilot-intellij", + extensionVersion: "1.5.0", + }, +}); + +await client.start(); +``` + +
+ +
+Python + + +```python +from copilot import CopilotClient + +client = CopilotClient( + client_info={ + "editor_name": "JetBrains-IU", + "editor_version": "2026.1", + "extension_name": "copilot-intellij", + "extension_version": "1.5.0", + }, +) +await client.start() +``` + +
+ +
+Go + + +```go +package main + +import ( + "context" + + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(&copilot.ClientOptions{ + ClientInfo: &copilot.ClientInfo{ + EditorName: "JetBrains-IU", + EditorVersion: "2026.1", + ExtensionName: "copilot-intellij", + ExtensionVersion: "1.5.0", + }, + }) + if err := client.Start(ctx); err != nil { + return + } +} +``` + + +```go +client := copilot.NewClient(&copilot.ClientOptions{ + ClientInfo: &copilot.ClientInfo{ + EditorName: "JetBrains-IU", + EditorVersion: "2026.1", + ExtensionName: "copilot-intellij", + ExtensionVersion: "1.5.0", + }, +}) +if err := client.Start(ctx); err != nil { + return err +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot; + +await using var client = new CopilotClient(new CopilotClientOptions +{ + ClientInfo = new CopilotClientInfo + { + EditorName = "JetBrains-IU", + EditorVersion = "2026.1", + ExtensionName = "copilot-intellij", + ExtensionVersion = "1.5.0", + }, +}); + +await client.StartAsync(); +``` + +
+ +
+Java + + +```java +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.ClientInfo; +import com.github.copilot.rpc.CopilotClientOptions; + +public class ClientInfoExample { + public static void main(String[] args) throws Exception { + var options = new CopilotClientOptions() + .setClientInfo(new ClientInfo() + .setEditorName("JetBrains-IU") + .setEditorVersion("2026.1") + .setExtensionName("copilot-intellij") + .setExtensionVersion("1.5.0")); + + var client = new CopilotClient(options); + client.start().get(); + } +} +``` + + +```java +var options = new CopilotClientOptions() + .setClientInfo(new ClientInfo() + .setEditorName("JetBrains-IU") + .setEditorVersion("2026.1") + .setExtensionName("copilot-intellij") + .setExtensionVersion("1.5.0")); + +var client = new CopilotClient(options); +client.start().get(); +``` + +
+ +
+Rust + + +```rust +use github_copilot_sdk::{Client, ClientInfo, ClientOptions}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let _client = Client::start( + ClientOptions::new().with_client_info( + ClientInfo::new() + .with_editor_name("JetBrains-IU") + .with_editor_version("2026.1") + .with_extension_name("copilot-intellij") + .with_extension_version("1.5.0"), + ), + ) + .await?; + Ok(()) +} +``` + + +```rust +use github_copilot_sdk::{Client, ClientInfo, ClientOptions}; + +let client = Client::start( + ClientOptions::new().with_client_info( + ClientInfo::new() + .with_editor_name("JetBrains-IU") + .with_editor_version("2026.1") + .with_extension_name("copilot-intellij") + .with_extension_version("1.5.0"), + ), +) +.await?; +``` + +
+ +## Notes + +* Client info is advisory. The runtime can ignore values that do not match the expected format, such as an invalid version string. +* Setting client info changes how the runtime attributes its telemetry. It does not change what the runtime records. +* If every field is unset or empty, the SDK omits client info from the handshake and the runtime keeps its default attribution. diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 141473f535..a23032a905 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -2182,7 +2182,11 @@ [new ConnectHandshakeRequest( // handler is registered (mirrors the runtime, which reads this flag on the // `connect` handshake so the first session's un-replayable `session.start` // event is forwarded). Also sent on session.create/resume for older CLIs. - _options.OnGitHubTelemetry != null ? true : null)], + _options.OnGitHubTelemetry != null ? true : null, + // Declare the integrating host's identity so the runtime attributes the + // telemetry it emits on this connection to a consistent surface instead + // of its own build. Null when the app didn't supply it. + ConnectHandshakeClientInfo.From(_options.ClientInfo))], connection.StderrBuffer, cancellationToken); serverVersion = (int)connectResponse.ProtocolVersion; @@ -3187,7 +3191,42 @@ internal record GetSessionMetadataResponse( internal record ConnectHandshakeRequest( string? Token, - [property: JsonPropertyName("enableGitHubTelemetryForwarding")] bool? EnableGitHubTelemetryForwarding = null); + [property: JsonPropertyName("enableGitHubTelemetryForwarding")] bool? EnableGitHubTelemetryForwarding = null, + [property: JsonPropertyName("clientInfo")] ConnectHandshakeClientInfo? ClientInfo = null); + + internal record ConnectHandshakeClientInfo( + [property: JsonPropertyName("editorName")] string? EditorName = null, + [property: JsonPropertyName("editorVersion")] string? EditorVersion = null, + [property: JsonPropertyName("extensionName")] string? ExtensionName = null, + [property: JsonPropertyName("extensionVersion")] string? ExtensionVersion = null) + { + /// + /// Maps the public onto the connect wire + /// shape, dropping empty fields. Returns when no + /// identity was supplied so the handshake omits clientInfo and the + /// runtime keeps its default attribution. + /// + public static ConnectHandshakeClientInfo? From(CopilotClientInfo? info) + { + if (info is null) + { + return null; + } + + var editorName = NullIfEmpty(info.EditorName); + var editorVersion = NullIfEmpty(info.EditorVersion); + var extensionName = NullIfEmpty(info.ExtensionName); + var extensionVersion = NullIfEmpty(info.ExtensionVersion); + if (editorName is null && editorVersion is null && extensionName is null && extensionVersion is null) + { + return null; + } + + return new ConnectHandshakeClientInfo(editorName, editorVersion, extensionName, extensionVersion); + } + + private static string? NullIfEmpty(string? value) => string.IsNullOrEmpty(value) ? null : value; + } internal record BuiltinPluginDirectoriesRequest( string[] Paths); @@ -3227,6 +3266,7 @@ internal record HooksInvokeResponse( [JsonSerializable(typeof(GetSessionMetadataRequest))] [JsonSerializable(typeof(GetSessionMetadataResponse))] [JsonSerializable(typeof(ConnectHandshakeRequest))] + [JsonSerializable(typeof(ConnectHandshakeClientInfo))] [JsonSerializable(typeof(BuiltinPluginDirectoriesRequest))] [JsonSerializable(typeof(McpOAuthTokenStorageMode))] [JsonSerializable(typeof(EmbeddingCacheStorageMode))] diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 8eb5e9bb2c..ec58000e97 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -322,6 +322,7 @@ private CopilotClientOptions(CopilotClientOptions? other) OnGitHubTelemetry = other.OnGitHubTelemetry; SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds; EnableRemoteSessions = other.EnableRemoteSessions; + ClientInfo = other.ClientInfo; Mode = other.Mode; } @@ -465,6 +466,16 @@ private CopilotClientOptions(CopilotClientOptions? other) /// public bool EnableRemoteSessions { get; set; } + /// + /// Declares the integrating host's identity, forwarded to the runtime on the + /// server.connect handshake. Declaring it lets the telemetry the + /// runtime emits on this connection be attributed to a consistent surface + /// (the host editor and its Copilot extension) instead of the runtime's own + /// build. All fields are optional; leave it to keep + /// the runtime's default attribution. + /// + public CopilotClientInfo? ClientInfo { get; set; } + /// /// Creates a shallow clone of this instance. /// @@ -531,6 +542,38 @@ public sealed class TelemetryConfig public bool? CaptureContent { get; set; } } +/// +/// Identifies the integrating host on the server.connect handshake. +/// +/// +/// Declaring it lets the telemetry the runtime emits on the connection be +/// attributed to a single, consistent surface instead of the runtime's own +/// build. All properties are optional; an unset property is omitted from the +/// handshake. +/// +public sealed class CopilotClientInfo +{ + /// + /// Name of the host editor, e.g. "vscode". + /// + public string? EditorName { get; set; } + + /// + /// Version of the host editor, e.g. "1.124.2". + /// + public string? EditorVersion { get; set; } + + /// + /// Name of the Copilot extension within the host, e.g. "copilot-chat". + /// + public string? ExtensionName { get; set; } + + /// + /// Version of the Copilot extension within the host, e.g. "0.54.0". + /// + public string? ExtensionVersion { get; set; } +} + /// /// Configuration for a custom session filesystem provider. /// diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 2948ac2129..2d25d4761f 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -23,6 +23,13 @@ public void CopilotClientOptions_Clone_CopiesAllProperties() BuiltinPluginDirectories = ["/plugins/core", "/plugins/github"], EnableRemoteSessions = true, SessionIdleTimeoutSeconds = 600, + ClientInfo = new CopilotClientInfo + { + EditorName = "example-editor", + EditorVersion = "1.0.0", + ExtensionName = "example-extension", + ExtensionVersion = "2.0.0", + }, }; var clone = original.Clone(); @@ -38,6 +45,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties() Assert.NotSame(original.BuiltinPluginDirectories, clone.BuiltinPluginDirectories); Assert.Equal(original.EnableRemoteSessions, clone.EnableRemoteSessions); Assert.Equal(original.SessionIdleTimeoutSeconds, clone.SessionIdleTimeoutSeconds); + Assert.Same(original.ClientInfo, clone.ClientInfo); } [Fact] diff --git a/dotnet/test/Unit/GitHubTelemetryTests.cs b/dotnet/test/Unit/GitHubTelemetryTests.cs index a4a241e38d..708e599d9f 100644 --- a/dotnet/test/Unit/GitHubTelemetryTests.cs +++ b/dotnet/test/Unit/GitHubTelemetryTests.cs @@ -140,6 +140,89 @@ public async Task Connect_Does_Not_Opt_In_Without_Handler() "connect request should omit enableGitHubTelemetryForwarding (or send null) when no handler is registered"); } + [Fact] + public async Task Connect_Forwards_Declared_ClientInfo() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + ClientInfo = new CopilotClientInfo + { + EditorName = "JetBrains-IU", + EditorVersion = "2026.1", + ExtensionName = "copilot-intellij", + ExtensionVersion = "1.5.0", + }, + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.True(connectParams.TryGetProperty("clientInfo", out var clientInfo)); + Assert.Equal("JetBrains-IU", clientInfo.GetProperty("editorName").GetString()); + Assert.Equal("2026.1", clientInfo.GetProperty("editorVersion").GetString()); + Assert.Equal("copilot-intellij", clientInfo.GetProperty("extensionName").GetString()); + Assert.Equal("1.5.0", clientInfo.GetProperty("extensionVersion").GetString()); + } + + [Fact] + public async Task Connect_Omits_ClientInfo_When_Unset() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.False( + connectParams.TryGetProperty("clientInfo", out _), + "connect request should omit clientInfo when none was declared"); + } + + [Fact] + public async Task Connect_Omits_Empty_ClientInfo_Fields() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + ClientInfo = new CopilotClientInfo { EditorName = "example-editor", EditorVersion = "" }, + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.True(connectParams.TryGetProperty("clientInfo", out var clientInfo)); + Assert.Equal("example-editor", clientInfo.GetProperty("editorName").GetString()); + Assert.False(clientInfo.TryGetProperty("editorVersion", out _)); + Assert.False(clientInfo.TryGetProperty("extensionName", out _)); + Assert.False(clientInfo.TryGetProperty("extensionVersion", out _)); + } + + [Fact] + public async Task Connect_Omits_All_Empty_ClientInfo() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + ClientInfo = new CopilotClientInfo + { + EditorName = "", + EditorVersion = "", + ExtensionName = "", + ExtensionVersion = "", + }, + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.False( + connectParams.TryGetProperty("clientInfo", out _), + "connect request should omit an all-empty clientInfo"); + } + [Fact] public async Task CreateSession_Does_Not_Opt_In_Without_Handler() { diff --git a/go/client.go b/go/client.go index 8f6b41a983..735e8ae414 100644 --- a/go/client.go +++ b/go/client.go @@ -1951,6 +1951,10 @@ func (c *Client) verifyProtocolVersion(ctx context.Context) error { if c.options.OnGitHubTelemetry != nil { connectReq.EnableGitHubTelemetryForwarding = Bool(true) } + // Declare the integrating host's identity so the runtime attributes the + // telemetry it emits on this connection to a consistent surface instead of + // its own build. Nil when the app didn't supply it. + connectReq.ClientInfo = c.options.ClientInfo.toWire() rawConnectResult, err := c.client.Request(ctx, "connect", connectReq) if err != nil { var rpcErr *jsonrpc2.Error @@ -1987,8 +1991,9 @@ func (c *Client) verifyProtocolVersion(ctx context.Context) error { } type connectHandshakeRequest struct { - Token *string `json:"token,omitempty"` - EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` + Token *string `json:"token,omitempty"` + EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` + ClientInfo *rpc.ConnectClientInfo `json:"clientInfo,omitempty"` } // stderrBufferSize is the maximum number of bytes kept from the CLI process's diff --git a/go/client_test.go b/go/client_test.go index bede29b496..99a21255dc 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -276,6 +276,106 @@ func TestClient_BuiltinPluginDirectories(t *testing.T) { }) } +func TestClient_ClientInfo(t *testing.T) { + findConnect := func(requests []startupRPCRequest) map[string]any { + t.Helper() + for _, request := range requests { + if request.Method != "connect" { + continue + } + var params map[string]any + if err := json.Unmarshal(request.Params, ¶ms); err != nil { + t.Fatalf("decode connect params: %v", err) + } + return params + } + t.Fatal("connect was not called") + return nil + } + + t.Run("forwards a declared identity on the connect handshake", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: url}, + ClientInfo: &ClientInfo{ + EditorName: "JetBrains-IU", + EditorVersion: "2026.1", + ExtensionName: "copilot-intellij", + ExtensionVersion: "1.5.0", + }, + }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + params := findConnect(requests()) + want := map[string]any{ + "editorName": "JetBrains-IU", + "editorVersion": "2026.1", + "extensionName": "copilot-intellij", + "extensionVersion": "1.5.0", + } + if !reflect.DeepEqual(params["clientInfo"], want) { + t.Fatalf("clientInfo = %v, want %v", params["clientInfo"], want) + } + }) + + t.Run("omits clientInfo when unset", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{Connection: URIConnection{URL: url}}) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + if _, ok := findConnect(requests())["clientInfo"]; ok { + t.Fatal("clientInfo should be omitted when unset") + } + }) + + t.Run("omits empty fields", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: url}, + ClientInfo: &ClientInfo{EditorName: "example-editor"}, + }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + want := map[string]any{"editorName": "example-editor"} + if got := findConnect(requests())["clientInfo"]; !reflect.DeepEqual(got, want) { + t.Fatalf("clientInfo = %v, want %v", got, want) + } + }) + + t.Run("omits clientInfo when all fields empty", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: url}, + ClientInfo: &ClientInfo{}, + }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + if _, ok := findConnect(requests())["clientInfo"]; ok { + t.Fatal("clientInfo should be omitted when all fields are empty") + } + }) +} + type startupRPCRequest struct { Method string Params json.RawMessage diff --git a/go/types.go b/go/types.go index 09bc281f22..a1deebd1d7 100644 --- a/go/types.go +++ b/go/types.go @@ -193,6 +193,13 @@ type ClientOptions struct { // directory are accessible from GitHub web and mobile. // Ignored when connecting to an existing runtime via [URIConnection]. EnableRemoteSessions bool + // ClientInfo declares the integrating host's identity, forwarded to the + // runtime on the `server.connect` handshake. Declaring it lets the + // telemetry the runtime emits on this connection be attributed to a + // consistent surface (the host editor and its Copilot extension) instead of + // the runtime's own build. All fields are optional; leave it nil to keep the + // runtime's default attribution. + ClientInfo *ClientInfo // Mode controls the default tool surface and feature flags presented to // sessions created by this client. The zero value ([ModeCopilotCli]) // matches legacy CLI defaults. Set to [ModeEmpty] to opt in to @@ -204,6 +211,55 @@ type ClientOptions struct { Mode ClientMode } +// ClientInfo identifies the integrating host on the `server.connect` handshake. +// +// Declaring it lets the telemetry the runtime emits on the connection be +// attributed to a single, consistent surface instead of the runtime's own +// build. All fields are optional; an empty field is omitted from the handshake. +type ClientInfo struct { + // EditorName is the name of the host editor, e.g. "vscode". + EditorName string + // EditorVersion is the version of the host editor, e.g. "1.124.2". + EditorVersion string + // ExtensionName is the name of the Copilot extension within the host, e.g. + // "copilot-chat". + ExtensionName string + // ExtensionVersion is the version of the Copilot extension within the host, + // e.g. "0.54.0". + ExtensionVersion string +} + +// toWire maps the public [ClientInfo] onto the generated connect wire shape, +// omitting empty fields. It returns nil when no identity was supplied so the +// caller drops the clientInfo field and keeps the runtime's default attribution. +func (ci *ClientInfo) toWire() *rpc.ConnectClientInfo { + if ci == nil { + return nil + } + wire := &rpc.ConnectClientInfo{} + populated := false + if ci.EditorName != "" { + wire.EditorName = &ci.EditorName + populated = true + } + if ci.EditorVersion != "" { + wire.EditorVersion = &ci.EditorVersion + populated = true + } + if ci.ExtensionName != "" { + wire.ExtensionName = &ci.ExtensionName + populated = true + } + if ci.ExtensionVersion != "" { + wire.ExtensionVersion = &ci.ExtensionVersion + populated = true + } + if !populated { + return nil + } + return wire +} + // CloudSessionRepository is GitHub repository metadata associated with a cloud session. type CloudSessionRepository struct { Owner string `json:"owner"` diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index 661fe6dab4..3818c82646 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -642,6 +642,13 @@ private void verifyProtocolVersion(Connection connection) throws Exception { if (this.options.getOnGitHubTelemetry() != null) { connectParams.put("enableGitHubTelemetryForwarding", true); } + // Declare the integrating host's identity so the runtime attributes the + // telemetry it emits on this connection to a consistent surface instead of + // its own build. Omitted when the app didn't supply it (or supplied no fields). + var clientInfo = this.options.getClientInfo(); + if (clientInfo != null && !clientInfo.isEmpty()) { + connectParams.put("clientInfo", clientInfo); + } var connectResponse = connection.rpc.invoke("connect", connectParams, ConnectResult.class).get(30, TimeUnit.SECONDS); serverVersion = connectResponse.protocolVersion() != null diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java new file mode 100644 index 0000000000..ebb54db6db --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Identity of the integrating host, declared on the {@code server.connect} + * handshake. + *

+ * Declaring it lets the telemetry the runtime emits on the connection be + * attributed to a single, consistent surface (the host editor and its Copilot + * extension) instead of the runtime's own build. All fields are optional; an + * empty field is omitted from the handshake. + * + *

Example Usage

+ * + *
{@code
+ * var options = new CopilotClientOptions().setClientInfo(new ClientInfo().setEditorName("vscode")
+ * 		.setEditorVersion("1.124.2").setExtensionName("copilot-chat").setExtensionVersion("0.54.0"));
+ * }
+ * + * @see CopilotClientOptions#setClientInfo(ClientInfo) + * @since 1.6.0 + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class ClientInfo { + + @JsonProperty("editorName") + private String editorName; + + @JsonProperty("editorVersion") + private String editorVersion; + + @JsonProperty("extensionName") + private String extensionName; + + @JsonProperty("extensionVersion") + private String extensionVersion; + + /** + * Gets the name of the host editor. + * + * @return the editor name (e.g., {@code "vscode"}), or {@code null} + */ + public String getEditorName() { + return editorName; + } + + /** + * Sets the name of the host editor. + * + * @param editorName + * the editor name (e.g., {@code "vscode"}) + * @return this client info for method chaining + */ + public ClientInfo setEditorName(String editorName) { + this.editorName = editorName; + return this; + } + + /** + * Gets the version of the host editor. + * + * @return the editor version (e.g., {@code "1.124.2"}), or {@code null} + */ + public String getEditorVersion() { + return editorVersion; + } + + /** + * Sets the version of the host editor. + * + * @param editorVersion + * the editor version (e.g., {@code "1.124.2"}) + * @return this client info for method chaining + */ + public ClientInfo setEditorVersion(String editorVersion) { + this.editorVersion = editorVersion; + return this; + } + + /** + * Gets the name of the Copilot extension within the host. + * + * @return the extension name (e.g., {@code "copilot-chat"}), or {@code null} + */ + public String getExtensionName() { + return extensionName; + } + + /** + * Sets the name of the Copilot extension within the host. + * + * @param extensionName + * the extension name (e.g., {@code "copilot-chat"}) + * @return this client info for method chaining + */ + public ClientInfo setExtensionName(String extensionName) { + this.extensionName = extensionName; + return this; + } + + /** + * Gets the version of the Copilot extension within the host. + * + * @return the extension version (e.g., {@code "0.54.0"}), or {@code null} + */ + public String getExtensionVersion() { + return extensionVersion; + } + + /** + * Sets the version of the Copilot extension within the host. + * + * @param extensionVersion + * the extension version (e.g., {@code "0.54.0"}) + * @return this client info for method chaining + */ + public ClientInfo setExtensionVersion(String extensionVersion) { + this.extensionVersion = extensionVersion; + return this; + } + + /** + * Returns whether no field carries a non-empty value, in which case the SDK + * omits {@code clientInfo} from the handshake so the runtime keeps its default + * attribution. + * + * @return {@code true} when every field is {@code null} or empty + */ + @JsonIgnore + public boolean isEmpty() { + return isBlank(editorName) && isBlank(editorVersion) && isBlank(extensionName) && isBlank(extensionVersion); + } + + private static boolean isBlank(String value) { + return value == null || value.isEmpty(); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java index a9c9cbcda7..3af76da52f 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java @@ -55,6 +55,7 @@ public class CopilotClientOptions { private String cliPath; private String cliUrl; private RuntimeConnection connection; + private ClientInfo clientInfo; private String copilotHome; private String cwd; private Map environment; @@ -678,6 +679,36 @@ public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) { return this; } + /** + * Gets the integrating host's declared identity. + * + * @return the client info, or {@code null} + * @since 1.6.0 + */ + public ClientInfo getClientInfo() { + return clientInfo; + } + + /** + * Declares the integrating host's identity, forwarded to the runtime on the + * {@code server.connect} handshake. + *

+ * Declaring it lets the telemetry the runtime emits on this connection be + * attributed to a consistent surface (the host editor and its Copilot + * extension) instead of the runtime's own build. All fields on + * {@link ClientInfo} are optional; leave this unset to keep the runtime's + * default attribution. + * + * @param clientInfo + * the host identity to declare + * @return this options instance for method chaining + * @since 1.6.0 + */ + public CopilotClientOptions setClientInfo(ClientInfo clientInfo) { + this.clientInfo = Objects.requireNonNull(clientInfo, "clientInfo must not be null"); + return this; + } + /** * Gets the server-wide idle timeout for sessions in seconds. * @@ -830,6 +861,7 @@ public CopilotClientOptions clone() { copy.cliPath = this.cliPath; copy.cliUrl = this.cliUrl; copy.connection = this.connection; + copy.clientInfo = this.clientInfo; copy.copilotHome = this.copilotHome; copy.cwd = this.cwd; copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; diff --git a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java index 7b0deb9977..af83fc7803 100644 --- a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java +++ b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.github.copilot.generated.rpc.GitHubTelemetryNotification; +import com.github.copilot.rpc.ClientInfo; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.ResumeSessionConfig; @@ -204,6 +205,95 @@ void clientOmitsForwardingWhenNoHandler() throws Exception { } } + @Test + void connectForwardsDeclaredClientInfo() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setEditorName("JetBrains-IU").setEditorVersion("2026.1") + .setExtensionName("copilot-intellij").setExtensionVersion("1.5.0")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + JsonNode clientInfo = connectParams.path("clientInfo"); + assertEquals(4, clientInfo.size(), "clientInfo should carry only the four declared fields"); + assertEquals("JetBrains-IU", clientInfo.path("editorName").asText()); + assertEquals("2026.1", clientInfo.path("editorVersion").asText()); + assertEquals("copilot-intellij", clientInfo.path("extensionName").asText()); + assertEquals("1.5.0", clientInfo.path("extensionVersion").asText()); + } + } + + @Test + void connectOmitsClientInfoWhenUnset() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + assertFalse(connectParams.has("clientInfo"), + "connect request should omit clientInfo when none was declared"); + } + } + + @Test + void connectOmitsEmptyClientInfoFields() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setEditorName("example-editor")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + JsonNode clientInfo = connectParams.path("clientInfo"); + assertEquals("example-editor", clientInfo.path("editorName").asText()); + assertFalse(clientInfo.has("editorVersion"), "unset editorVersion should be omitted"); + assertFalse(clientInfo.has("extensionName"), "unset extensionName should be omitted"); + assertFalse(clientInfo.has("extensionVersion"), "unset extensionVersion should be omitted"); + } + } + + @Test + void connectDropsEmptyClientInfoFields() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setEditorName("vscode").setEditorVersion("")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + JsonNode clientInfo = connectParams.path("clientInfo"); + assertEquals(1, clientInfo.size(), "clientInfo should carry only the non-empty field"); + assertEquals("vscode", clientInfo.path("editorName").asText()); + assertFalse(clientInfo.has("editorVersion"), "empty editorVersion should be dropped"); + } + } + + @Test + void connectOmitsAllEmptyClientInfo() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setEditorName("").setEditorVersion("").setExtensionName("") + .setExtensionVersion("")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + assertFalse(connectParams.has("clientInfo"), "connect request should omit an all-empty clientInfo"); + } + } + + @Test + void optionsRetainAndCloneClientInfo() { + var info = new ClientInfo().setEditorName("example-editor"); + var options = new CopilotClientOptions().setClientInfo(info); + assertSame(info, options.getClientInfo()); + + var copy = options.clone(); + assertSame(info, copy.getClientInfo()); + } + @Test void optionsRetainAndCloneTelemetryHandler() { Function> handler = n -> CompletableFuture diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index acaf67ba85..eb8391e1dd 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -34,6 +34,7 @@ import { registerClientSessionApiHandlers, } from "./generated/rpc.js"; import type { + ConnectClientInfo, GitHubTelemetryNotification, GitHubTokenAcquireRequest, GitHubTokenAcquireResult, @@ -52,6 +53,7 @@ import { ToolSet } from "./toolSet.js"; import type { AutoModeSwitchRequest, AutoModeSwitchResponse, + CopilotClientInfo, CopilotClientMode, CopilotClientOptions, CustomAgentConfig, @@ -259,6 +261,22 @@ function toWireCustomAgents(agents: CustomAgentConfig[] | undefined): unknown[] }); } +/** + * Map the public {@link CopilotClientInfo} onto the generated connect wire + * shape, dropping empty fields. Returns `undefined` when no field carries a + * non-empty value so the caller omits `clientInfo` from the handshake and keeps + * the runtime's default attribution. + */ +function clientInfoToWire(info: CopilotClientInfo | undefined): ConnectClientInfo | undefined { + if (info == null) return undefined; + const wire: ConnectClientInfo = {}; + if (info.editorName) wire.editorName = info.editorName; + if (info.editorVersion) wire.editorVersion = info.editorVersion; + if (info.extensionName) wire.extensionName = info.extensionName; + if (info.extensionVersion) wire.extensionVersion = info.extensionVersion; + return Object.keys(wire).length > 0 ? wire : undefined; +} + /** * Convert a {@link LargeToolOutputConfig} from the public API shape * (`outputDirectory`) to the wire shape (`outputDir`). @@ -522,6 +540,7 @@ export class CopilotClient { sessionIdleTimeoutSeconds: number; enableRemoteSessions: boolean; mode: CopilotClientMode; + clientInfo?: CopilotClientInfo; }; private isExternalServer: boolean = false; private forceStopping: boolean = false; @@ -778,6 +797,7 @@ export class CopilotClient { sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0, enableRemoteSessions: options.enableRemoteSessions ?? false, mode: options.mode ?? "copilot-cli", + clientInfo: options.clientInfo, }; // Empty mode: validate at construction time that the app supplied a @@ -2218,6 +2238,7 @@ export class CopilotClient { const connectParams: { token?: string; enableGitHubTelemetryForwarding?: boolean; + clientInfo?: ConnectClientInfo; } = { token: this.effectiveConnectionToken }; // Opt in to GitHub telemetry forwarding at the connection level when a // handler is registered (mirrors the runtime, which reads this flag on the @@ -2226,6 +2247,14 @@ export class CopilotClient { if (this.onGitHubTelemetry != null) { connectParams.enableGitHubTelemetryForwarding = true; } + // Declare the integrating host's identity so the runtime attributes + // the telemetry it emits on this connection to a consistent surface + // instead of its own build. Empty fields are dropped, and an + // all-empty identity is omitted entirely. + const clientInfo = clientInfoToWire(this.options.clientInfo); + if (clientInfo != null) { + connectParams.clientInfo = clientInfo; + } const result = await raceAgainstExit(this.internalRpc.connect(connectParams)); serverVersion = result.protocolVersion; } catch (err) { diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 11e742bee7..bf6f2195f9 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -69,6 +69,7 @@ export type { UserPromptTransformedHandler, UserPromptTransformedHookInput, UserPromptTransformedHookOutput, + CopilotClientInfo, CopilotClientMode, CopilotClientOptions, CopilotExpAssignmentResponse, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index cd211fd9f5..efed11194a 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -307,6 +307,37 @@ export type InternalRuntimeConnection = RuntimeConnection | ParentProcessRuntime */ export type CopilotClientMode = "empty" | "copilot-cli"; +/** + * Identity of the integrating host, declared once on the `server.connect` + * handshake so the telemetry the runtime emits on this connection is attributed + * to a single, consistent surface rather than to the runtime's own build. + * + * All fields are optional; omit any of them (or the whole object) to keep the + * runtime's default attribution. Version fields are ignored by the runtime + * unless they look like a version string. + */ +export interface CopilotClientInfo { + /** + * Name of the host editor, e.g. `"vscode"`. + */ + editorName?: string; + + /** + * Version of the host editor, e.g. `"1.124.2"`. + */ + editorVersion?: string; + + /** + * Name of the Copilot extension within the host, e.g. `"copilot-chat"`. + */ + extensionName?: string; + + /** + * Version of the Copilot extension within the host, e.g. `"0.54.0"`. + */ + extensionVersion?: string; +} + export interface CopilotClientOptions { /** * How to connect to the Copilot runtime. When omitted, defaults to @@ -478,6 +509,16 @@ export interface CopilotClientOptions { */ enableRemoteSessions?: boolean; + /** + * Identity of the integrating host, forwarded to the runtime on the + * `server.connect` handshake. Declaring it lets the telemetry the runtime + * emits on this connection be attributed to a single, consistent surface + * (e.g. the host editor and its Copilot extension) instead of the runtime's + * own build. All fields are optional; omit it to keep the default + * attribution. + */ + clientInfo?: CopilotClientInfo; + /** * @internal Hook used by `joinSession()` to construct a client that talks * to its parent process over stdio. Not part of the public API. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 829d69f2e5..cef9cbf490 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -13,6 +13,7 @@ import { DisableBypassPermissionsModes, RuntimeConnection, type CapiSessionOptions, + type CopilotClientOptions, type GitHubTelemetryNotification, type ManagedSettings, type ModelInfo, @@ -4244,3 +4245,76 @@ describe("managedSettings serialization", () => { }); }); }); + +describe("connect handshake clientInfo", () => { + // Drives verifyProtocolVersion() against a stubbed connection so we can + // observe the `connect` params without spawning a runtime. `connect` maps to + // connection.sendRequest("connect", params) in the generated internal RPC. + async function captureConnectParams( + options: Partial> = {} + ): Promise> { + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("localhost:1234"), + ...options, + }); + const sendRequest = vi.fn(async (method: string, _params?: unknown) => { + if (method === "connect") return { protocolVersion: 3 }; + throw new Error(`Unexpected method: ${method}`); + }); + (client as any).connection = { sendRequest }; + + await (client as any).verifyProtocolVersion(); + + const connectCall = sendRequest.mock.calls.find(([method]) => method === "connect"); + expect(connectCall, "connect was not called").toBeTruthy(); + return connectCall![1] as Record; + } + + it("forwards a declared client identity on the connect handshake", async () => { + const clientInfo = { + editorName: "JetBrains-IU", + editorVersion: "2026.1", + extensionName: "copilot-intellij", + extensionVersion: "1.5.0", + }; + + const params = await captureConnectParams({ clientInfo }); + + expect(params.clientInfo).toEqual(clientInfo); + }); + + it("omits clientInfo from the handshake when the host declares none", async () => { + const params = await captureConnectParams(); + + expect(params).not.toHaveProperty("clientInfo"); + }); + + it("drops empty fields and omits an all-empty identity", async () => { + const allEmpty = await captureConnectParams({ + clientInfo: { + editorName: "", + editorVersion: "", + extensionName: "", + extensionVersion: "", + }, + }); + expect(allEmpty).not.toHaveProperty("clientInfo"); + + const partial = await captureConnectParams({ + clientInfo: { editorName: "vscode", editorVersion: "" }, + }); + expect(partial.clientInfo).toEqual({ editorName: "vscode" }); + }); + + it("keeps telemetry forwarding alongside a declared identity", async () => { + const params = await captureConnectParams({ + clientInfo: { editorName: "example-editor" }, + onGitHubTelemetry: () => {}, + }); + + expect(params).toMatchObject({ + clientInfo: { editorName: "example-editor" }, + enableGitHubTelemetryForwarding: true, + }); + }); +}); diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 5325b937c5..5d9e3f9500 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -33,6 +33,7 @@ AutoTier, CapiSessionOptions, ChildProcessRuntimeConnection, + ClientInfo, CloudSessionOptions, CloudSessionRepository, CopilotClient, @@ -244,6 +245,7 @@ "CanvasProviderIdentity", "CapiSessionOptions", "ChildProcessRuntimeConnection", + "ClientInfo", "CloudSessionOptions", "CloudSessionRepository", "CommandContext", diff --git a/python/copilot/client.py b/python/copilot/client.py index a15c1a569e..4db29d1e78 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -507,6 +507,46 @@ class TelemetryConfig(TypedDict, total=False): """Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501 +class ClientInfo(TypedDict, total=False): + """Identity of the integrating host, declared on the ``server.connect`` handshake. + + Declaring it lets the telemetry the runtime emits on this connection be + attributed to a single, consistent surface (the host editor and its Copilot + extension) instead of the runtime's own build. All fields are optional; omit + any of them (or the whole object) to keep the runtime's default attribution. + """ + + editor_name: str + """Name of the host editor, e.g. ``"vscode"``.""" + editor_version: str + """Version of the host editor, e.g. ``"1.124.2"``.""" + extension_name: str + """Name of the Copilot extension within the host, e.g. ``"copilot-chat"``.""" + extension_version: str + """Version of the Copilot extension within the host, e.g. ``"0.54.0"``.""" + + +def _client_info_to_wire(client_info: ClientInfo | None) -> dict[str, str] | None: + """Map a snake_case :class:`ClientInfo` onto the camelCase connect wire shape. + + Empty fields are dropped. Returns ``None`` when no field carries a non-empty + value so the caller omits the ``clientInfo`` field entirely and keeps the + runtime's default attribution. + """ + if not client_info: + return None + wire: dict[str, str] = {} + if client_info.get("editor_name"): + wire["editorName"] = client_info["editor_name"] + if client_info.get("editor_version"): + wire["editorVersion"] = client_info["editor_version"] + if client_info.get("extension_name"): + wire["extensionName"] = client_info["extension_name"] + if client_info.get("extension_version"): + wire["extensionVersion"] = client_info["extension_version"] + return wire or None + + @dataclass class RuntimeConnection: """Discriminated config describing how to reach the Copilot runtime. @@ -776,6 +816,7 @@ class _CopilotClientOptions: request_handler: CopilotRequestHandler | None = None session_idle_timeout_seconds: int | None = None enable_remote_sessions: bool = False + client_info: ClientInfo | None = None on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None on_github_telemetry: Callable[[GitHubTelemetryNotification], None | Awaitable[None]] | None = ( None @@ -1530,6 +1571,7 @@ def __init__( request_handler: CopilotRequestHandler | None = None, session_idle_timeout_seconds: int | None = None, enable_remote_sessions: bool = False, + client_info: ClientInfo | None = None, on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None, on_github_telemetry: Callable[[GitHubTelemetryNotification], None | Awaitable[None]] | None = None, @@ -1579,6 +1621,11 @@ def __init__( Control integration). When ``True``, sessions in a GitHub repository working directory are accessible from GitHub web and mobile. + client_info: Identity of the integrating host, forwarded to the + runtime on the ``server.connect`` handshake. Declaring it lets + the telemetry the runtime emits on this connection be attributed + to a consistent surface instead of the runtime's own build. All + fields are optional; omit it to keep the default attribution. on_list_models: Custom handler for :meth:`list_models`. When provided, the handler is called instead of querying the runtime server. @@ -1616,6 +1663,7 @@ def __init__( request_handler=request_handler, session_idle_timeout_seconds=session_idle_timeout_seconds, enable_remote_sessions=enable_remote_sessions, + client_info=client_info, on_list_models=on_list_models, on_github_telemetry=on_github_telemetry, mode=mode, @@ -4054,6 +4102,12 @@ async def _verify_protocol_version(self) -> None: # event is forwarded). Also sent on session.create/resume for older CLIs. if self._on_github_telemetry is not None: connect_params["enableGitHubTelemetryForwarding"] = True + # Declare the integrating host's identity so the runtime attributes + # the telemetry it emits on this connection to a consistent surface + # instead of its own build. Omitted when the app didn't supply it. + client_info = _client_info_to_wire(self._options.client_info) + if client_info is not None: + connect_params["clientInfo"] = client_info connect_result = _ConnectResult.from_dict( await self._client.request("connect", connect_params) ) diff --git a/python/test_client.py b/python/test_client.py index d1c6ebe1d7..197103dd68 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -3151,6 +3151,105 @@ async def request(self, method, params, **kwargs): await client._verify_protocol_version() assert "enableGitHubTelemetryForwarding" not in captured["connect"] + @pytest.mark.asyncio + async def test_connect_forwards_client_info(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={ + "editor_name": "JetBrains-IU", + "editor_version": "2026.1", + "extension_name": "copilot-intellij", + "extension_version": "1.5.0", + }, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert captured["connect"]["clientInfo"] == { + "editorName": "JetBrains-IU", + "editorVersion": "2026.1", + "extensionName": "copilot-intellij", + "extensionVersion": "1.5.0", + } + + @pytest.mark.asyncio + async def test_connect_omits_client_info_when_unset(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert "clientInfo" not in captured["connect"] + + @pytest.mark.asyncio + async def test_connect_forwards_partial_client_info_with_forwarding(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={"editor_name": "example-editor"}, + on_github_telemetry=lambda _notification: None, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert captured["connect"]["clientInfo"] == {"editorName": "example-editor"} + assert captured["connect"]["enableGitHubTelemetryForwarding"] is True + + @pytest.mark.asyncio + async def test_connect_drops_empty_client_info_fields(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={"editor_name": "vscode", "editor_version": ""}, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert captured["connect"]["clientInfo"] == {"editorName": "vscode"} + + @pytest.mark.asyncio + async def test_connect_omits_all_empty_client_info(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={ + "editor_name": "", + "editor_version": "", + "extension_name": "", + "extension_version": "", + }, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert "clientInfo" not in captured["connect"] + @pytest.mark.asyncio async def test_event_routes_to_handler(self): from copilot.generated.rpc import GitHubTelemetryNotification diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 32d44ddc0f..9734417e20 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -401,6 +401,99 @@ pub struct ClientOptions { /// (the default) or are stripped to a minimal/safe baseline. See /// [`ClientMode`] for the contract and trade-offs. pub mode: ClientMode, + /// Declares the integrating host's identity, forwarded to the runtime on + /// the `server.connect` handshake. Declaring it lets the telemetry the + /// runtime emits on this connection be attributed to a consistent surface + /// (the host editor and its Copilot extension) instead of the runtime's own + /// build. All fields are optional; leave it `None` to keep the runtime's + /// default attribution. + pub client_info: Option, +} + +/// Identity of the integrating host, declared on the `server.connect` +/// handshake. +/// +/// Declaring it lets the telemetry the runtime emits on the connection be +/// attributed to a single, consistent surface instead of the runtime's own +/// build. All fields are optional; an empty field is omitted from the +/// handshake. +/// +/// The struct is `#[non_exhaustive]`, so construct it with [`ClientInfo::new`] +/// and the `with_*` builder methods rather than a struct literal. This lets the +/// SDK add identity fields in future releases without a breaking change. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[non_exhaustive] +pub struct ClientInfo { + /// Name of the host editor, e.g. `"vscode"`. + pub editor_name: Option, + /// Version of the host editor, e.g. `"1.124.2"`. + pub editor_version: Option, + /// Name of the Copilot extension within the host, e.g. `"copilot-chat"`. + pub extension_name: Option, + /// Version of the Copilot extension within the host, e.g. `"0.54.0"`. + pub extension_version: Option, +} + +impl ClientInfo { + /// Create an empty `ClientInfo`. Populate fields with the `with_*` builder + /// methods; every field is optional. + pub fn new() -> Self { + Self::default() + } + + /// Set the host editor name, e.g. `"vscode"`. + pub fn with_editor_name(mut self, editor_name: impl Into) -> Self { + self.editor_name = Some(editor_name.into()); + self + } + + /// Set the host editor version, e.g. `"1.124.2"`. + pub fn with_editor_version(mut self, editor_version: impl Into) -> Self { + self.editor_version = Some(editor_version.into()); + self + } + + /// Set the Copilot extension name within the host, e.g. `"copilot-chat"`. + pub fn with_extension_name(mut self, extension_name: impl Into) -> Self { + self.extension_name = Some(extension_name.into()); + self + } + + /// Set the Copilot extension version within the host, e.g. `"0.54.0"`. + pub fn with_extension_version(mut self, extension_version: impl Into) -> Self { + self.extension_version = Some(extension_version.into()); + self + } + + /// Returns `true` when no field carries a non-empty value, in which case the + /// SDK omits `clientInfo` from the handshake and the runtime keeps its + /// default attribution. + fn is_empty(&self) -> bool { + Self::non_empty(&self.editor_name).is_none() + && Self::non_empty(&self.editor_version).is_none() + && Self::non_empty(&self.extension_name).is_none() + && Self::non_empty(&self.extension_version).is_none() + } + + /// Clone the field only when it holds a non-empty string, so empty fields are + /// dropped from the handshake. + fn non_empty(value: &Option) -> Option { + value.as_ref().filter(|s| !s.is_empty()).cloned() + } + + /// Map onto the generated connect wire shape, dropping empty fields. Returns + /// `None` when no field carries a non-empty value. + fn to_wire(&self) -> Option { + if self.is_empty() { + return None; + } + Some(crate::generated::api_types::ConnectClientInfo { + editor_name: Self::non_empty(&self.editor_name), + editor_version: Self::non_empty(&self.editor_version), + extension_name: Self::non_empty(&self.extension_name), + extension_version: Self::non_empty(&self.extension_version), + }) + } } impl std::fmt::Debug for ClientOptions { @@ -452,6 +545,7 @@ impl std::fmt::Debug for ClientOptions { .field("base_directory", &self.base_directory) .field("enable_remote_sessions", &self.enable_remote_sessions) .field("bundled_cli_extract_dir", &self.bundled_cli_extract_dir) + .field("client_info", &self.client_info) .finish() } } @@ -701,6 +795,7 @@ impl Default for ClientOptions { enable_remote_sessions: false, bundled_cli_extract_dir: None, mode: ClientMode::default(), + client_info: None, } } } @@ -931,6 +1026,14 @@ impl ClientOptions { self.mode = mode; self } + + /// Declare the integrating host's identity, forwarded to the runtime on + /// the `server.connect` handshake so its telemetry is attributed to a + /// consistent surface. See [`Self::client_info`]. + pub fn with_client_info(mut self, client_info: ClientInfo) -> Self { + self.client_info = Some(client_info); + self + } } /// Validate a [`SessionFsConfig`] before sending `sessionFs.setProvider`. @@ -1100,6 +1203,10 @@ struct ClientInner { /// `None` for stdio and for external-server transport without an /// explicit token. effective_connection_token: Option, + /// Host identity forwarded on the `connect` handshake, set from + /// [`ClientOptions::client_info`]. `None` keeps the runtime's default + /// telemetry attribution. + client_info: Option, /// SDK [`ClientMode`] captured at start time. Drives empty-mode safe /// defaults inside `create_session` / `resume_session`. pub(crate) mode: ClientMode, @@ -1314,6 +1421,7 @@ impl Client { options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )? } Transport::Tcp { @@ -1347,6 +1455,7 @@ impl Client { options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )? } Transport::Stdio => { @@ -1370,6 +1479,7 @@ impl Client { options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )? } Transport::InProcess => { @@ -1437,6 +1547,7 @@ impl Client { options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )?; *client.inner.ffi_host.lock() = Some(shared); client @@ -1579,6 +1690,7 @@ impl Client { None, None, ClientMode::default(), + None, ) } @@ -1606,6 +1718,7 @@ impl Client { None, None, ClientMode::default(), + None, ) } @@ -1637,6 +1750,7 @@ impl Client { None, None, ClientMode::default(), + None, ) } @@ -1664,6 +1778,7 @@ impl Client { None, token, ClientMode::default(), + None, ) } @@ -1691,6 +1806,7 @@ impl Client { Some(on_github_telemetry), None, ClientMode::default(), + None, ) } @@ -1704,6 +1820,35 @@ impl Client { generate_connection_token() } + /// Construct a [`Client`] from raw streams with a preset + /// [`ClientInfo`], for integration testing the `connect` handshake's + /// host-identity forwarding path. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_client_info( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + client_info: Option, + ) -> Result { + Self::from_transport( + reader, + writer, + None, + None, + cwd, + None, + None, + false, + false, + None, + None, + None, + ClientMode::default(), + client_info, + ) + } + #[allow(clippy::too_many_arguments)] fn from_transport( reader: impl AsyncRead + Unpin + Send + 'static, @@ -1721,6 +1866,7 @@ impl Client { on_github_telemetry: Option, effective_connection_token: Option, mode: ClientMode, + client_info: Option, ) -> Result { let setup_start = Instant::now(); let (request_tx, request_rx) = mpsc::unbounded_channel::(); @@ -1766,6 +1912,7 @@ impl Client { on_get_trace_context, effective_connection_token, mode, + client_info, startup_timings: OnceLock::new(), }), }; @@ -2327,7 +2474,15 @@ impl Client { .on_github_telemetry .is_some() .then_some(true), - ..Default::default() + // Declare the integrating host's identity so the runtime attributes + // the telemetry it emits on this connection to a consistent surface + // instead of its own build. `None` when the app didn't supply it, and + // empty fields are dropped. + client_info: self + .inner + .client_info + .as_ref() + .and_then(ClientInfo::to_wire), }; let value = self .call( @@ -3485,6 +3640,7 @@ mod tests { None, None, ClientMode::default(), + None, ) .unwrap(); @@ -3595,6 +3751,7 @@ mod tests { on_get_trace_context: None, effective_connection_token: None, mode: ClientMode::default(), + client_info: None, startup_timings: OnceLock::new(), }), } diff --git a/rust/tests/protocol_version_test.rs b/rust/tests/protocol_version_test.rs index 0d1268c59e..33b6c8d155 100644 --- a/rust/tests/protocol_version_test.rs +++ b/rust/tests/protocol_version_test.rs @@ -239,3 +239,153 @@ async fn connect_handshake_forwards_auto_generated_token() { .unwrap() .unwrap(); } + +/// Positive coverage for host-identity forwarding on the `connect` +/// handshake. A client constructed with a [`ClientInfo`] MUST serialize it +/// (camelCase) into the outbound `connect` request's `clientInfo` param so +/// the runtime attributes this connection's telemetry to the host surface. +#[tokio::test] +async fn connect_handshake_forwards_client_info() { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_client_info( + client_read, + client_write, + std::env::temp_dir(), + Some( + github_copilot_sdk::ClientInfo::new() + .with_editor_name("JetBrains-IU") + .with_editor_version("2026.1") + .with_extension_name("copilot-intellij") + .with_extension_version("1.5.0"), + ), + ) + .unwrap(); + + let mut server_read = server_read; + let mut server_write = server_write; + + let verify_handle = tokio::spawn({ + let client = client.clone(); + async move { client.verify_protocol_version().await } + }); + + let req = read_framed(&mut server_read).await; + assert_eq!(req["method"], "connect"); + let client_info = &req["params"]["clientInfo"]; + assert_eq!(client_info["editorName"], "JetBrains-IU"); + assert_eq!(client_info["editorVersion"], "2026.1"); + assert_eq!(client_info["extensionName"], "copilot-intellij"); + assert_eq!(client_info["extensionVersion"], "1.5.0"); + + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": req["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test-1.0.0" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + tokio::time::timeout(std::time::Duration::from_secs(2), verify_handle) + .await + .unwrap() + .unwrap() + .unwrap(); +} + +/// A [`ClientInfo`] with only some fields set must omit the empty ones from +/// the wire, and a fully-empty one must drop `clientInfo` entirely so the +/// runtime keeps its default attribution. +#[tokio::test] +async fn connect_handshake_omits_empty_client_info_fields() { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_client_info( + client_read, + client_write, + std::env::temp_dir(), + Some( + github_copilot_sdk::ClientInfo::new() + .with_editor_name("example-editor") + .with_editor_version(""), + ), + ) + .unwrap(); + + let mut server_read = server_read; + let mut server_write = server_write; + + let verify_handle = tokio::spawn({ + let client = client.clone(); + async move { client.verify_protocol_version().await } + }); + + let req = read_framed(&mut server_read).await; + assert_eq!(req["method"], "connect"); + let client_info = &req["params"]["clientInfo"]; + assert_eq!(client_info["editorName"], "example-editor"); + assert!(client_info.get("editorVersion").is_none()); + assert!(client_info.get("extensionName").is_none()); + assert!(client_info.get("extensionVersion").is_none()); + + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": req["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test-1.0.0" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + tokio::time::timeout(std::time::Duration::from_secs(2), verify_handle) + .await + .unwrap() + .unwrap() + .unwrap(); +} + +/// A [`ClientInfo`] whose every field is empty must drop `clientInfo` from the +/// handshake entirely so the runtime keeps its default attribution. +#[tokio::test] +async fn connect_handshake_omits_all_empty_client_info() { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_client_info( + client_read, + client_write, + std::env::temp_dir(), + Some( + github_copilot_sdk::ClientInfo::new() + .with_editor_name("") + .with_editor_version("") + .with_extension_name("") + .with_extension_version(""), + ), + ) + .unwrap(); + + let mut server_read = server_read; + let mut server_write = server_write; + + let verify_handle = tokio::spawn({ + let client = client.clone(); + async move { client.verify_protocol_version().await } + }); + + let req = read_framed(&mut server_read).await; + assert_eq!(req["method"], "connect"); + assert!( + req["params"].get("clientInfo").is_none(), + "an all-empty clientInfo must be omitted from the handshake" + ); + + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": req["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test-1.0.0" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + tokio::time::timeout(std::time::Duration::from_secs(2), verify_handle) + .await + .unwrap() + .unwrap() + .unwrap(); +} From 975cf6bb97912fa9abe4a11a3a4a3a7dd9045e41 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 2 Sep 2026 11:21:15 +0100 Subject: [PATCH 23/60] ci: reduce SDK CI critical paths and runner saturation (#2480) --- .github/workflows/dotnet-sdk-tests.yml | 74 +++++---- .github/workflows/go-sdk-tests.yml | 4 +- .github/workflows/java-sdk-tests.yml | 18 +-- .github/workflows/nodejs-sdk-tests.yml | 3 - .github/workflows/python-sdk-tests.yml | 4 +- .github/workflows/required-checks.yml | 4 + .github/workflows/rust-sdk-tests.yml | 140 ++++++++++++++---- .../workflows/sdk-consistency-review.lock.yml | 12 +- .github/workflows/sdk-consistency-review.md | 5 + go/internal/e2e/rpc_server_e2e_test.go | 1 + java/sdk/pom.xml | 2 + 11 files changed, 183 insertions(+), 84 deletions(-) diff --git a/.github/workflows/dotnet-sdk-tests.yml b/.github/workflows/dotnet-sdk-tests.yml index 4695cae7f2..6ac2a0d7c9 100644 --- a/.github/workflows/dotnet-sdk-tests.yml +++ b/.github/workflows/dotnet-sdk-tests.yml @@ -1,9 +1,6 @@ name: ".NET SDK Tests" on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -11,6 +8,40 @@ permissions: contents: read jobs: + validate: + name: ".NET SDK Build and Format" + if: github.event.repository.fork == false + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./dotnet + steps: + - uses: actions/checkout@v6.0.2 + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.0.x" + - uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Restore .NET dependencies + run: dotnet restore + + - name: Run dotnet format check + run: | + if ! dotnet format --no-restore --verify-no-changes; then + echo "❌ dotnet format produced changes. Please run 'dotnet format' in dotnet" + exit 1 + fi + echo "✅ dotnet format produced no changes" + + # Build the whole solution once to validate every SDK target framework. + # Matrix cells below only need to build frameworks consumed by the tests. + - name: Build SDK + run: dotnet build --no-restore + test: name: ".NET SDK Tests (${{ matrix.os }}, ${{ matrix.transport }}, ${{ matrix.backend }}, ${{ matrix.shard }})" if: github.event.repository.fork == false @@ -97,7 +128,7 @@ jobs: # A hung test used to run until the runner died (~50 min) and the dying # runner never uploaded its logs, so the failures were undiagnosable. # Every healthy cell finishes well under 15 min. - timeout-minutes: 30 + timeout-minutes: 20 defaults: run: shell: bash @@ -111,27 +142,16 @@ jobs: with: node-version: "22" cache: "npm" - cache-dependency-path: "./nodejs/package-lock.json" + cache-dependency-path: | + ./nodejs/package-lock.json + ./test/harness/package-lock.json - - name: Install Node.js dependencies (for CLI version extraction) + - name: Install Node.js dependencies working-directory: ./nodejs run: npm ci --ignore-scripts - name: Restore .NET dependencies - run: dotnet restore - - - name: Run dotnet format check - if: runner.os == 'Linux' - run: | - dotnet format --verify-no-changes - if [ $? -ne 0 ]; then - echo "❌ dotnet format produced changes. Please run 'dotnet format' in dotnet" - exit 1 - fi - echo "✅ dotnet format produced no changes" - - - name: Build SDK - run: dotnet build --no-restore + run: dotnet restore test/GitHub.Copilot.SDK.Test.csproj - name: Install test harness dependencies working-directory: ./test/harness @@ -153,17 +173,21 @@ jobs: # --blame-hang names the offending test instead of letting it wedge # the runner. No single test legitimately runs for 10 minutes; the # whole suite normally finishes in about five. - args=(--no-build -v n --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type none) + # The validation job performs the full analyzer-enabled SDK build. + # Build only test-consumed frameworks here and do not repeat analyzers. + args=(--no-restore -v n --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type none -p:RunAnalyzers=false) filter="$DOTNET_TEST_FILTER" if [[ "$DOTNET_TEST_SHARD" != "full" ]]; then case "$DOTNET_TEST_SHARD" in 1) - initials=(A C D H I J K L N Q S U W Y) + # M moved here after recent runs showed shard 2 was about two + # minutes slower; this keeps Windows and macOS balanced. + initials=(A C D H I J K L M N Q S U W Y) shard_filter="FullyQualifiedName~GitHub.Copilot.Test.ConnectionToken" ;; 2) - initials=(B E F G M O P R T V X Z) + initials=(B E F G O P R T V X Z) shard_filter="" ;; 2a) @@ -171,7 +195,7 @@ jobs: shard_filter="" ;; 2b) - initials=(M O P R) + initials=(O P R) shard_filter="" ;; 2c) @@ -192,4 +216,4 @@ jobs: if [[ -n "$filter" ]]; then args+=(--filter "$filter") fi - dotnet test "${args[@]}" + dotnet test test/GitHub.Copilot.SDK.Test.csproj "${args[@]}" diff --git a/.github/workflows/go-sdk-tests.yml b/.github/workflows/go-sdk-tests.yml index 61d74d257e..4fb6fd0184 100644 --- a/.github/workflows/go-sdk-tests.yml +++ b/.github/workflows/go-sdk-tests.yml @@ -1,9 +1,6 @@ name: "Go SDK Tests" on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -22,6 +19,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] transport: ["default", "inprocess"] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index 36310d26b5..321c5c4f55 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -1,15 +1,6 @@ name: "Java SDK Tests" on: - push: - branches: - - main - paths: - - "java/**" - - "test/**" - - ".github/workflows/java-sdk-tests.yml" - - ".github/actions/setup-copilot/**" - - ".github/actions/java-test-report/**" workflow_dispatch: workflow_call: @@ -63,7 +54,7 @@ jobs: run: mvn clean verify -Pinprocess ${{ matrix.maven-args }} - name: Generate Test Report Summary - if: always() + if: failure() uses: ./.github/actions/java-test-report with: title: "Copilot Java SDK :: Test Results InProcess" @@ -481,9 +472,8 @@ jobs: if: matrix.test-jdk == '25' env: CI: "true" - run: | - node copilot-native/scripts/validate-native-host.mjs linux-x64 - mvn verify -Dskip.test.harness=true -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=false + # Native artifacts are built and exercised by every classifier job. + run: mvn -pl sdk verify -Dskip.test.harness=true - name: Switch to JDK 17 if: matrix.test-jdk == '17' @@ -502,7 +492,7 @@ jobs: mvn -pl sdk jacoco:prepare-agent@wire-up-coverage-instrumentation antrun:run@print-test-jdk-banner surefire:test failsafe:integration-test failsafe:verify jacoco:report@build-coverage-report-from-tests -Denforcer.skip=true - name: Upload test results for site generation - if: success() && github.ref == 'refs/heads/main' && matrix.test-jdk == '25' + if: success() && github.event_name == 'merge_group' && matrix.test-jdk == '25' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-for-site diff --git a/.github/workflows/nodejs-sdk-tests.yml b/.github/workflows/nodejs-sdk-tests.yml index 4c31f79cc4..7647bf35d1 100644 --- a/.github/workflows/nodejs-sdk-tests.yml +++ b/.github/workflows/nodejs-sdk-tests.yml @@ -4,9 +4,6 @@ env: HUSKY: 0 on: - push: - branches: - - main workflow_dispatch: workflow_call: diff --git a/.github/workflows/python-sdk-tests.yml b/.github/workflows/python-sdk-tests.yml index 1ea9739756..3c0644a7c0 100644 --- a/.github/workflows/python-sdk-tests.yml +++ b/.github/workflows/python-sdk-tests.yml @@ -4,9 +4,6 @@ env: PYTHONUTF8: 1 on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -27,6 +24,7 @@ jobs: python-version: ["3.11"] transport: ["default", "inprocess"] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash diff --git a/.github/workflows/required-checks.yml b/.github/workflows/required-checks.yml index dbd79fd4cf..f4083dd307 100644 --- a/.github/workflows/required-checks.yml +++ b/.github/workflows/required-checks.yml @@ -10,6 +10,10 @@ permissions: contents: read pull-requests: read +concurrency: + group: sdk-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: changes: name: Select SDK workflows diff --git a/.github/workflows/rust-sdk-tests.yml b/.github/workflows/rust-sdk-tests.yml index 7fdac3b818..da030ff999 100644 --- a/.github/workflows/rust-sdk-tests.yml +++ b/.github/workflows/rust-sdk-tests.yml @@ -1,9 +1,6 @@ name: "Rust SDK Tests" on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -23,6 +20,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -38,17 +36,6 @@ jobs: uses: dtolnay/rust-toolchain@stable with: toolchain: "1.94.0" - components: rustfmt, clippy - - # Nightly rustfmt for unstable format options (group_imports, - # imports_granularity, reorder_impl_items) — pinned in - # `.rustfmt.nightly.toml`. - - name: Install nightly rustfmt - if: runner.os == 'Linux' - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2026-04-14 - components: rustfmt - uses: Swatinem/rust-cache@v2 with: @@ -73,23 +60,6 @@ jobs: path: ./rust/.bundled-cli-cache key: bundled-cli-${{ matrix.os }}-${{ steps.cli-version.outputs.version }} - - name: cargo fmt --check (nightly) - if: runner.os == 'Linux' - run: cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --check - - - name: cargo clippy - if: runner.os == 'Linux' - env: - BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache - run: cargo clippy --all-targets --features test-support,bundled-in-process -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type - - - name: cargo doc - if: runner.os == 'Linux' - env: - RUSTDOCFLAGS: "-D warnings" - BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache - run: cargo doc --no-deps --all-features - - name: Install test harness dependencies working-directory: ./test/harness run: npm ci --ignore-scripts @@ -112,6 +82,112 @@ jobs: # The dedicated `bundle` job below exercises the embed pipeline. run: cargo test --no-default-features --features test-support -- --test-threads=4 --nocapture + clippy: + name: "Rust SDK Format and Clippy" + if: github.event.repository.fork == false + env: + POWERSHELL_UPDATECHECK: Off + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./rust + steps: + - uses: actions/checkout@v6.0.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: "1.94.0" + components: clippy + + # Nightly rustfmt for unstable format options (group_imports, + # imports_granularity, reorder_impl_items) — pinned in + # `.rustfmt.nightly.toml`. + - name: Install nightly rustfmt + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: nightly-2026-04-14 + components: rustfmt + + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 + with: + workspaces: "rust" + prefix-key: v1-rust-no-bin + cache-bin: false + + - name: Read pinned @github/copilot CLI version + id: cli-version + working-directory: ./nodejs + run: | + version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Pinned CLI version: $version" + + - name: Cache bundled CLI archives + uses: actions/cache@v4 + with: + path: ./rust/.bundled-cli-cache + key: bundled-cli-ubuntu-latest-${{ steps.cli-version.outputs.version }} + + - name: cargo fmt --check (nightly) + run: cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --check + + - name: cargo clippy + env: + BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache + run: cargo clippy --all-targets --features test-support,bundled-in-process -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type + + doc: + name: "Rust SDK Docs" + if: github.event.repository.fork == false + env: + POWERSHELL_UPDATECHECK: Off + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./rust + steps: + - uses: actions/checkout@v6.0.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: "1.94.0" + + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 + with: + workspaces: "rust" + prefix-key: v1-rust-no-bin + cache-bin: false + + - name: Read pinned @github/copilot CLI version + id: cli-version + working-directory: ./nodejs + run: | + version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Pinned CLI version: $version" + + - name: Cache bundled CLI archives + uses: actions/cache@v4 + with: + path: ./rust/.bundled-cli-cache + key: bundled-cli-ubuntu-latest-${{ steps.cli-version.outputs.version }} + + - name: cargo doc + env: + RUSTDOCFLAGS: "-D warnings" + BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache + run: cargo doc --no-deps --all-features + # Exercises the in-process FFI transport (`Transport::InProcess`, the Rust # analogue of the .NET `RuntimeConnection.ForInProcess()`), mirroring the # `inprocess` transport cell in dotnet-sdk-tests.yml. Sets @@ -133,6 +209,7 @@ jobs: # TODO: Re-enable Windows after fixing the napi-oop peer shutdown crash. os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -207,6 +284,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash diff --git a/.github/workflows/sdk-consistency-review.lock.yml b/.github/workflows/sdk-consistency-review.lock.yml index bc33be9ad1..4480d40559 100644 --- a/.github/workflows/sdk-consistency-review.lock.yml +++ b/.github/workflows/sdk-consistency-review.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fb73d13f101fc375308576a64180f63934cc9e8306cb6ef6303f1b9788d9df28","body_hash":"cc60c817de34cdb662ae4c091203c67a5ef240ca0165b0cd26a842f03b22614f","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"79b4a25722897db4bbdb7fa862f579bd3c37e12d0ce147e46d618ab79bce67c4","body_hash":"cc60c817de34cdb662ae4c091203c67a5ef240ca0165b0cd26a842f03b22614f","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","detection_agent_id":"copilot","detection_agent_model":"${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","engine_versions":{"copilot":"1.0.73"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}],"has_pull_request":true} # This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -134,7 +134,7 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" GH_AW_INFO_VERSION: "1.0.73" GH_AW_INFO_AGENT_VERSION: "1.0.73" GH_AW_INFO_CLI_VERSION: "v0.83.1" @@ -863,10 +863,11 @@ jobs: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} @@ -1467,10 +1468,11 @@ jobs: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 @@ -1564,7 +1566,7 @@ jobs: GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" - GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} diff --git a/.github/workflows/sdk-consistency-review.md b/.github/workflows/sdk-consistency-review.md index 550d9349d0..4d1c7e4511 100644 --- a/.github/workflows/sdk-consistency-review.md +++ b/.github/workflows/sdk-consistency-review.md @@ -1,6 +1,7 @@ --- description: Reviews PRs to ensure features are implemented consistently across all SDK language implementations tracker-id: sdk-consistency-review +model: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} on: roles: all pull_request: @@ -35,6 +36,10 @@ safe-outputs: max: 1 hide-older-comments: true allowed-reasons: [outdated] + threat-detection: + engine: + id: copilot + model: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} timeout-minutes: 15 --- diff --git a/go/internal/e2e/rpc_server_e2e_test.go b/go/internal/e2e/rpc_server_e2e_test.go index 6ea9ad6851..ed38fe1b80 100644 --- a/go/internal/e2e/rpc_server_e2e_test.go +++ b/go/internal/e2e/rpc_server_e2e_test.go @@ -532,6 +532,7 @@ func TestRPCServerE2E(t *testing.T) { t.Run("should report implemented error when connecting unknown remote session", func(t *testing.T) { ctx := testharness.NewTestContext(t) + ctx.ConfigureWithoutSnapshot(t) client := ctx.NewClient() t.Cleanup(func() { client.ForceStop() }) if err := client.Start(t.Context()); err != nil { diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index bb7cd2fa36..73026237e9 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -632,6 +632,8 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the **/AskUserTest.java **/CompactionTest.java + + **/CopilotClientTest.java **/CopilotSessionTest.java **/ErrorHandlingTest.java **/EventFidelityTest.java From 27ca965d88ea6de643398446f79ae169fd92d170 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:23:58 +0000 Subject: [PATCH 24/60] build(deps): bump tsx (#2479) Bumps the java-codegen-deps group in /java/scripts/codegen with 1 update: [tsx](https://github.com/privatenumber/tsx). Updates `tsx` from 4.23.12 to 4.23.13 - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.23.12...v4.23.13) --- updated-dependencies: - dependency-name: tsx dependency-version: 4.23.13 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: java-codegen-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- java/scripts/codegen/package-lock.json | 8 ++++---- java/scripts/codegen/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 2c4712269a..8d3292a1b0 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "@github/copilot": "^1.0.83-1", "json-schema": "^0.4.0", - "tsx": "^4.23.12" + "tsx": "^4.23.13" } }, "node_modules/@esbuild/aix-ppc64": { @@ -648,9 +648,9 @@ "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/tsx": { - "version": "4.23.12", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", - "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "license": "MIT", "dependencies": { "esbuild": "~0.28.0" diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 2e2e1f7e19..4e7c52ff8a 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -9,6 +9,6 @@ "dependencies": { "@github/copilot": "^1.0.83-1", "json-schema": "^0.4.0", - "tsx": "^4.23.12" + "tsx": "^4.23.13" } } From aae14627ac2bf7881c433bdd8d5b9ec0ad38c9f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:39:36 +0100 Subject: [PATCH 25/60] build(deps): bump the java-maven-deps group across 1 directory with 4 updates (#2406) Bumps the java-maven-deps group with 4 updates in the /java directory: [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin), [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson-databind), com.fasterxml.jackson.datatype:jackson-datatype-jsr310 and [com.github.spotbugs:spotbugs-annotations](https://github.com/spotbugs/spotbugs). Updates `com.github.spotbugs:spotbugs-maven-plugin` from 4.10.3.0 to 4.10.4.0 - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.10.3.0...spotbugs-maven-plugin-4.10.4.0) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.22.1 to 2.22.2 - [Commits](https://github.com/FasterXML/jackson-databind/compare/jackson-databind-2.22.1...jackson-databind-2.22.2) Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.22.1 to 2.22.2 Updates `com.github.spotbugs:spotbugs-annotations` from 4.10.3 to 4.10.4 - [Release notes](https://github.com/spotbugs/spotbugs/releases) - [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotbugs/spotbugs/compare/4.10.3...4.10.4) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.22.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: java-maven-deps - dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency-version: 2.22.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: java-maven-deps - dependency-name: com.github.spotbugs:spotbugs-annotations dependency-version: 4.10.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: java-maven-deps - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.10.4.0 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: java-maven-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- java/pom.xml | 2 +- java/sdk/pom.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/java/pom.xml b/java/pom.xml index 30d9d582d6..f053f39ad7 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -139,7 +139,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.10.3.0 + 4.10.4.0 com.diffplug.spotless diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index 73026237e9..dd22744f48 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -79,7 +79,7 @@ com.fasterxml.jackson.core jackson-databind - 2.22.1 + 2.22.2 com.fasterxml.jackson.core @@ -89,14 +89,14 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.22.1 + 2.22.2 com.github.spotbugs spotbugs-annotations - 4.10.3 + 4.10.4 provided From 040530f9f3a8c96099b794df6b83553b548d1dea Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 2 Sep 2026 14:56:56 +0100 Subject: [PATCH 26/60] Use application terminology for client info (#2482) * Rename client info fields for applications Use application and integration terminology in every public SDK while preserving the existing runtime wire protocol. Update tests and documentation to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Apply Java source formatting Match the repository's Spotless line wrapping for the updated client identity documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarify client info examples Keep the IDE example across languages, document application-only usage, and explain integrations as optional extensions, plugins, or other application sub-parts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarify integration field documentation Describe integration identity consistently across all six SDKs as an optional application extension, plugin, or other named sub-part. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Apply Java comment formatting Match Spotless wrapping for the clarified integration accessor documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 +- docs/features/README.md | 2 +- docs/features/client-info.md | 96 +++++++-------- dotnet/src/Client.cs | 10 +- dotnet/src/Types.cs | 22 ++-- dotnet/test/Unit/CloneTests.cs | 8 +- dotnet/test/Unit/GitHubTelemetryTests.cs | 26 ++--- go/client_test.go | 18 +-- go/types.go | 41 ++++--- .../com/github/copilot/CopilotClient.java | 2 +- .../com/github/copilot/rpc/ClientInfo.java | 109 ++++++++++-------- .../copilot/rpc/CopilotClientOptions.java | 12 +- .../github/copilot/GitHubTelemetryTest.java | 25 ++-- nodejs/src/client.ts | 10 +- nodejs/src/types.ts | 24 ++-- nodejs/test/client.test.ts | 31 ++--- python/copilot/client.py | 44 +++---- python/test_client.py | 30 ++--- rust/src/lib.rs | 73 ++++++------ rust/tests/protocol_version_test.rs | 32 ++--- 20 files changed, 318 insertions(+), 301 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e0c94d66..f7e753beda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu ## [Unreleased] -### Feature: declare host identity with client info +### Feature: declare application identity with client info -Client options now accept optional client info (editor name and version, extension name and version) across all six SDKs, exposed idiomatically per language (`clientInfo` in Node.js, `client_info` in Python and Rust, `ClientInfo` in Go and .NET, `setClientInfo` in Java). When set, the SDK forwards it on the `server.connect` handshake so the telemetry the runtime emits on the connection is attributed to the host editor and its Copilot extension instead of the runtime's own build. All fields are optional, and leaving client info unset keeps the runtime's default attribution. See [Client info](./docs/features/client-info.md). +Client options now accept optional client info (application name and version, integration name and version) across all six SDKs, exposed idiomatically per language (`clientInfo` in Node.js, `client_info` in Python and Rust, `ClientInfo` in Go and .NET, `setClientInfo` in Java). When set, the SDK forwards it on the `server.connect` handshake so the telemetry the runtime emits on the connection is attributed to the application and its Copilot integration instead of the runtime's own build. All fields are optional, and leaving client info unset keeps the runtime's default attribution. See [Client info](./docs/features/client-info.md). ### Feature: Node Agent Factories pagination and run notifications diff --git a/docs/features/README.md b/docs/features/README.md index 6350958720..5ea070a5aa 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -20,7 +20,7 @@ These guides cover the capabilities you can add to your Copilot SDK application. | [Image Input](./image-input.md) | Send images to sessions as attachments | | [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) | | [Usage and Billing](./usage-and-billing.md) | Read token counts, context-window utilization, AI credit cost, and account quota | -| [Client info](./client-info.md) | Declare the host editor and extension so runtime telemetry is attributed to your surface | +| [Client info](./client-info.md) | Declare application and integration identity for runtime telemetry attribution | | [Steering & Queueing](./steering-and-queueing.md) | Control message delivery—immediate steering vs. sequential queueing | | [Context Clearing](./context-management.md) | Replace conversation context safely with terminal tools | | [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage | diff --git a/docs/features/client-info.md b/docs/features/client-info.md index 48ada2d13a..f72780e41b 100644 --- a/docs/features/client-info.md +++ b/docs/features/client-info.md @@ -1,21 +1,23 @@ # Client info -Client info identifies the editor or extension that hosts your Copilot SDK application. Set the optional `clientInfo` client option to attribute runtime telemetry for that connection to your host instead of the runtime's own build. +Client info identifies the application using the Copilot SDK and, when applicable, a specific integration within it. An integration is an identifiable sub-part of the application through which the SDK is used, such as an extension or plugin. Set the optional `clientInfo` client option to attribute runtime telemetry for that connection to your application instead of the runtime's own build. ## When to set client info -Set client info when you embed the SDK in an editor, an extension, or another application with its own identity. +Set client info when your SDK application represents a distinct product, service, or integration whose runtime activity should be attributed consistently. -Leave client info unset for scripts, one-off tools, and back-end jobs that do not represent a distinct host. The runtime then keeps its default attribution. +Leave client info unset for scripts, one-off tools, and jobs that do not represent a distinct application. The runtime then keeps its default attribution. Client info has four optional string fields. Set the fields you know and omit the rest. The SDK includes client info in the `server.connect` handshake only when at least one field has a non-empty value. | Field | Example | Meaning | |---|---|---| -| `editorName` | `"vscode"` | Name of the host editor | -| `editorVersion` | `"1.124.2"` | Version of the host editor | -| `extensionName` | `"copilot-chat"` | Name of the Copilot extension within the host | -| `extensionVersion` | `"0.54.0"` | Version of the Copilot extension within the host | +| `applicationName` | `"vscode"` | Name of the application using the SDK | +| `applicationVersion` | `"1.124.2"` | Version of the application using the SDK | +| `integrationName` | `"copilot-chat"` | Name of the extension, plugin, or other application sub-part using the SDK | +| `integrationVersion` | `"0.54.0"` | Version of that extension, plugin, or application sub-part | + +For a standalone application without a distinct integration, set only the application fields. For example, a developer portal could set `applicationName` to `"acme-developer-portal"` and `applicationVersion` to `"2.4.0"`, leaving both integration fields unset. The SDK sends client info once when it establishes the connection. The identity applies for the lifetime of that connection. @@ -33,10 +35,10 @@ import { CopilotClient } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ clientInfo: { - editorName: "JetBrains-IU", - editorVersion: "2026.1", - extensionName: "copilot-intellij", - extensionVersion: "1.5.0", + applicationName: "vscode", + applicationVersion: "1.124.2", + integrationName: "copilot-chat", + integrationVersion: "0.54.0", }, }); @@ -52,10 +54,10 @@ import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient({ clientInfo: { - editorName: "JetBrains-IU", - editorVersion: "2026.1", - extensionName: "copilot-intellij", - extensionVersion: "1.5.0", + applicationName: "vscode", + applicationVersion: "1.124.2", + integrationName: "copilot-chat", + integrationVersion: "0.54.0", }, }); @@ -73,10 +75,10 @@ from copilot import CopilotClient client = CopilotClient( client_info={ - "editor_name": "JetBrains-IU", - "editor_version": "2026.1", - "extension_name": "copilot-intellij", - "extension_version": "1.5.0", + "application_name": "vscode", + "application_version": "1.124.2", + "integration_name": "copilot-chat", + "integration_version": "0.54.0", }, ) await client.start() @@ -101,10 +103,10 @@ func main() { ctx := context.Background() client := copilot.NewClient(&copilot.ClientOptions{ ClientInfo: &copilot.ClientInfo{ - EditorName: "JetBrains-IU", - EditorVersion: "2026.1", - ExtensionName: "copilot-intellij", - ExtensionVersion: "1.5.0", + ApplicationName: "vscode", + ApplicationVersion: "1.124.2", + IntegrationName: "copilot-chat", + IntegrationVersion: "0.54.0", }, }) if err := client.Start(ctx); err != nil { @@ -117,10 +119,10 @@ func main() { ```go client := copilot.NewClient(&copilot.ClientOptions{ ClientInfo: &copilot.ClientInfo{ - EditorName: "JetBrains-IU", - EditorVersion: "2026.1", - ExtensionName: "copilot-intellij", - ExtensionVersion: "1.5.0", + ApplicationName: "vscode", + ApplicationVersion: "1.124.2", + IntegrationName: "copilot-chat", + IntegrationVersion: "0.54.0", }, }) if err := client.Start(ctx); err != nil { @@ -140,10 +142,10 @@ await using var client = new CopilotClient(new CopilotClientOptions { ClientInfo = new CopilotClientInfo { - EditorName = "JetBrains-IU", - EditorVersion = "2026.1", - ExtensionName = "copilot-intellij", - ExtensionVersion = "1.5.0", + ApplicationName = "vscode", + ApplicationVersion = "1.124.2", + IntegrationName = "copilot-chat", + IntegrationVersion = "0.54.0", }, }); @@ -165,10 +167,10 @@ public class ClientInfoExample { public static void main(String[] args) throws Exception { var options = new CopilotClientOptions() .setClientInfo(new ClientInfo() - .setEditorName("JetBrains-IU") - .setEditorVersion("2026.1") - .setExtensionName("copilot-intellij") - .setExtensionVersion("1.5.0")); + .setApplicationName("vscode") + .setApplicationVersion("1.124.2") + .setIntegrationName("copilot-chat") + .setIntegrationVersion("0.54.0")); var client = new CopilotClient(options); client.start().get(); @@ -180,10 +182,10 @@ public class ClientInfoExample { ```java var options = new CopilotClientOptions() .setClientInfo(new ClientInfo() - .setEditorName("JetBrains-IU") - .setEditorVersion("2026.1") - .setExtensionName("copilot-intellij") - .setExtensionVersion("1.5.0")); + .setApplicationName("vscode") + .setApplicationVersion("1.124.2") + .setIntegrationName("copilot-chat") + .setIntegrationVersion("0.54.0")); var client = new CopilotClient(options); client.start().get(); @@ -203,10 +205,10 @@ async fn main() -> Result<(), Box> { let _client = Client::start( ClientOptions::new().with_client_info( ClientInfo::new() - .with_editor_name("JetBrains-IU") - .with_editor_version("2026.1") - .with_extension_name("copilot-intellij") - .with_extension_version("1.5.0"), + .with_application_name("vscode") + .with_application_version("1.124.2") + .with_integration_name("copilot-chat") + .with_integration_version("0.54.0"), ), ) .await?; @@ -221,10 +223,10 @@ use github_copilot_sdk::{Client, ClientInfo, ClientOptions}; let client = Client::start( ClientOptions::new().with_client_info( ClientInfo::new() - .with_editor_name("JetBrains-IU") - .with_editor_version("2026.1") - .with_extension_name("copilot-intellij") - .with_extension_version("1.5.0"), + .with_application_name("vscode") + .with_application_version("1.124.2") + .with_integration_name("copilot-chat") + .with_integration_version("0.54.0"), ), ) .await?; diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index a23032a905..92650ee60c 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -2183,7 +2183,7 @@ [new ConnectHandshakeRequest( // `connect` handshake so the first session's un-replayable `session.start` // event is forwarded). Also sent on session.create/resume for older CLIs. _options.OnGitHubTelemetry != null ? true : null, - // Declare the integrating host's identity so the runtime attributes the + // Declare the integrating application's identity so the runtime attributes the // telemetry it emits on this connection to a consistent surface instead // of its own build. Null when the app didn't supply it. ConnectHandshakeClientInfo.From(_options.ClientInfo))], @@ -3213,10 +3213,10 @@ internal record ConnectHandshakeClientInfo( return null; } - var editorName = NullIfEmpty(info.EditorName); - var editorVersion = NullIfEmpty(info.EditorVersion); - var extensionName = NullIfEmpty(info.ExtensionName); - var extensionVersion = NullIfEmpty(info.ExtensionVersion); + var editorName = NullIfEmpty(info.ApplicationName); + var editorVersion = NullIfEmpty(info.ApplicationVersion); + var extensionName = NullIfEmpty(info.IntegrationName); + var extensionVersion = NullIfEmpty(info.IntegrationVersion); if (editorName is null && editorVersion is null && extensionName is null && extensionVersion is null) { return null; diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index ec58000e97..3af7ea7f12 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -467,10 +467,10 @@ private CopilotClientOptions(CopilotClientOptions? other) public bool EnableRemoteSessions { get; set; } ///

- /// Declares the integrating host's identity, forwarded to the runtime on the + /// Declares the integrating application's identity, forwarded to the runtime on the /// server.connect handshake. Declaring it lets the telemetry the /// runtime emits on this connection be attributed to a consistent surface - /// (the host editor and its Copilot extension) instead of the runtime's own + /// (the application and its Copilot integration) instead of the runtime's own /// build. All fields are optional; leave it to keep /// the runtime's default attribution. /// @@ -543,7 +543,7 @@ public sealed class TelemetryConfig } /// -/// Identifies the integrating host on the server.connect handshake. +/// Identifies the integrating application on the server.connect handshake. /// /// /// Declaring it lets the telemetry the runtime emits on the connection be @@ -554,24 +554,24 @@ public sealed class TelemetryConfig public sealed class CopilotClientInfo { /// - /// Name of the host editor, e.g. "vscode". + /// Name of the application using the SDK. /// - public string? EditorName { get; set; } + public string? ApplicationName { get; set; } /// - /// Version of the host editor, e.g. "1.124.2". + /// Version of the application using the SDK. /// - public string? EditorVersion { get; set; } + public string? ApplicationVersion { get; set; } /// - /// Name of the Copilot extension within the host, e.g. "copilot-chat". + /// Optionally specifies a named integration within the application, such as an extension or plugin. /// - public string? ExtensionName { get; set; } + public string? IntegrationName { get; set; } /// - /// Version of the Copilot extension within the host, e.g. "0.54.0". + /// Optionally specifies the version of that integration. /// - public string? ExtensionVersion { get; set; } + public string? IntegrationVersion { get; set; } } /// diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 2d25d4761f..2f213525f6 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -25,10 +25,10 @@ public void CopilotClientOptions_Clone_CopiesAllProperties() SessionIdleTimeoutSeconds = 600, ClientInfo = new CopilotClientInfo { - EditorName = "example-editor", - EditorVersion = "1.0.0", - ExtensionName = "example-extension", - ExtensionVersion = "2.0.0", + ApplicationName = "example-app", + ApplicationVersion = "1.0.0", + IntegrationName = "example-integration", + IntegrationVersion = "2.0.0", }, }; diff --git a/dotnet/test/Unit/GitHubTelemetryTests.cs b/dotnet/test/Unit/GitHubTelemetryTests.cs index 708e599d9f..6d76af5742 100644 --- a/dotnet/test/Unit/GitHubTelemetryTests.cs +++ b/dotnet/test/Unit/GitHubTelemetryTests.cs @@ -149,19 +149,19 @@ public async Task Connect_Forwards_Declared_ClientInfo() Connection = RuntimeConnection.ForUri(server.Url), ClientInfo = new CopilotClientInfo { - EditorName = "JetBrains-IU", - EditorVersion = "2026.1", - ExtensionName = "copilot-intellij", - ExtensionVersion = "1.5.0", + ApplicationName = "acme-developer-portal", + ApplicationVersion = "2.4.0", + IntegrationName = "copilot-assistant", + IntegrationVersion = "1.5.0", }, }); await client.StartAsync(); var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); Assert.True(connectParams.TryGetProperty("clientInfo", out var clientInfo)); - Assert.Equal("JetBrains-IU", clientInfo.GetProperty("editorName").GetString()); - Assert.Equal("2026.1", clientInfo.GetProperty("editorVersion").GetString()); - Assert.Equal("copilot-intellij", clientInfo.GetProperty("extensionName").GetString()); + Assert.Equal("acme-developer-portal", clientInfo.GetProperty("editorName").GetString()); + Assert.Equal("2.4.0", clientInfo.GetProperty("editorVersion").GetString()); + Assert.Equal("copilot-assistant", clientInfo.GetProperty("extensionName").GetString()); Assert.Equal("1.5.0", clientInfo.GetProperty("extensionVersion").GetString()); } @@ -188,13 +188,13 @@ public async Task Connect_Omits_Empty_ClientInfo_Fields() await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url), - ClientInfo = new CopilotClientInfo { EditorName = "example-editor", EditorVersion = "" }, + ClientInfo = new CopilotClientInfo { ApplicationName = "example-app", ApplicationVersion = "" }, }); await client.StartAsync(); var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); Assert.True(connectParams.TryGetProperty("clientInfo", out var clientInfo)); - Assert.Equal("example-editor", clientInfo.GetProperty("editorName").GetString()); + Assert.Equal("example-app", clientInfo.GetProperty("editorName").GetString()); Assert.False(clientInfo.TryGetProperty("editorVersion", out _)); Assert.False(clientInfo.TryGetProperty("extensionName", out _)); Assert.False(clientInfo.TryGetProperty("extensionVersion", out _)); @@ -209,10 +209,10 @@ public async Task Connect_Omits_All_Empty_ClientInfo() Connection = RuntimeConnection.ForUri(server.Url), ClientInfo = new CopilotClientInfo { - EditorName = "", - EditorVersion = "", - ExtensionName = "", - ExtensionVersion = "", + ApplicationName = "", + ApplicationVersion = "", + IntegrationName = "", + IntegrationVersion = "", }, }); await client.StartAsync(); diff --git a/go/client_test.go b/go/client_test.go index 99a21255dc..e74c234637 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -300,10 +300,10 @@ func TestClient_ClientInfo(t *testing.T) { client := NewClient(&ClientOptions{ Connection: URIConnection{URL: url}, ClientInfo: &ClientInfo{ - EditorName: "JetBrains-IU", - EditorVersion: "2026.1", - ExtensionName: "copilot-intellij", - ExtensionVersion: "1.5.0", + ApplicationName: "acme-developer-portal", + ApplicationVersion: "2.4.0", + IntegrationName: "copilot-assistant", + IntegrationVersion: "1.5.0", }, }) if err := client.Start(t.Context()); err != nil { @@ -313,9 +313,9 @@ func TestClient_ClientInfo(t *testing.T) { params := findConnect(requests()) want := map[string]any{ - "editorName": "JetBrains-IU", - "editorVersion": "2026.1", - "extensionName": "copilot-intellij", + "editorName": "acme-developer-portal", + "editorVersion": "2.4.0", + "extensionName": "copilot-assistant", "extensionVersion": "1.5.0", } if !reflect.DeepEqual(params["clientInfo"], want) { @@ -344,14 +344,14 @@ func TestClient_ClientInfo(t *testing.T) { client := NewClient(&ClientOptions{ Connection: URIConnection{URL: url}, - ClientInfo: &ClientInfo{EditorName: "example-editor"}, + ClientInfo: &ClientInfo{ApplicationName: "example-app"}, }) if err := client.Start(t.Context()); err != nil { t.Fatalf("Start failed: %v", err) } defer client.ForceStop() - want := map[string]any{"editorName": "example-editor"} + want := map[string]any{"editorName": "example-app"} if got := findConnect(requests())["clientInfo"]; !reflect.DeepEqual(got, want) { t.Fatalf("clientInfo = %v, want %v", got, want) } diff --git a/go/types.go b/go/types.go index a1deebd1d7..772202a0bc 100644 --- a/go/types.go +++ b/go/types.go @@ -193,10 +193,10 @@ type ClientOptions struct { // directory are accessible from GitHub web and mobile. // Ignored when connecting to an existing runtime via [URIConnection]. EnableRemoteSessions bool - // ClientInfo declares the integrating host's identity, forwarded to the + // ClientInfo declares the integrating application's identity, forwarded to the // runtime on the `server.connect` handshake. Declaring it lets the // telemetry the runtime emits on this connection be attributed to a - // consistent surface (the host editor and its Copilot extension) instead of + // consistent surface (the application and its Copilot integration) instead of // the runtime's own build. All fields are optional; leave it nil to keep the // runtime's default attribution. ClientInfo *ClientInfo @@ -211,22 +211,21 @@ type ClientOptions struct { Mode ClientMode } -// ClientInfo identifies the integrating host on the `server.connect` handshake. +// ClientInfo identifies the integrating application on the `server.connect` handshake. // // Declaring it lets the telemetry the runtime emits on the connection be // attributed to a single, consistent surface instead of the runtime's own // build. All fields are optional; an empty field is omitted from the handshake. type ClientInfo struct { - // EditorName is the name of the host editor, e.g. "vscode". - EditorName string - // EditorVersion is the version of the host editor, e.g. "1.124.2". - EditorVersion string - // ExtensionName is the name of the Copilot extension within the host, e.g. - // "copilot-chat". - ExtensionName string - // ExtensionVersion is the version of the Copilot extension within the host, - // e.g. "0.54.0". - ExtensionVersion string + // ApplicationName is the name of the application using the SDK. + ApplicationName string + // ApplicationVersion is the version of the application using the SDK. + ApplicationVersion string + // IntegrationName optionally identifies a specific integration within the + // application, such as an extension or plugin. + IntegrationName string + // IntegrationVersion is the optional version of the named integration. + IntegrationVersion string } // toWire maps the public [ClientInfo] onto the generated connect wire shape, @@ -238,20 +237,20 @@ func (ci *ClientInfo) toWire() *rpc.ConnectClientInfo { } wire := &rpc.ConnectClientInfo{} populated := false - if ci.EditorName != "" { - wire.EditorName = &ci.EditorName + if ci.ApplicationName != "" { + wire.EditorName = &ci.ApplicationName populated = true } - if ci.EditorVersion != "" { - wire.EditorVersion = &ci.EditorVersion + if ci.ApplicationVersion != "" { + wire.EditorVersion = &ci.ApplicationVersion populated = true } - if ci.ExtensionName != "" { - wire.ExtensionName = &ci.ExtensionName + if ci.IntegrationName != "" { + wire.ExtensionName = &ci.IntegrationName populated = true } - if ci.ExtensionVersion != "" { - wire.ExtensionVersion = &ci.ExtensionVersion + if ci.IntegrationVersion != "" { + wire.ExtensionVersion = &ci.IntegrationVersion populated = true } if !populated { diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index 3818c82646..c986e829a4 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -642,7 +642,7 @@ private void verifyProtocolVersion(Connection connection) throws Exception { if (this.options.getOnGitHubTelemetry() != null) { connectParams.put("enableGitHubTelemetryForwarding", true); } - // Declare the integrating host's identity so the runtime attributes the + // Declare the integrating application's identity so the runtime attributes the // telemetry it emits on this connection to a consistent surface instead of // its own build. Omitted when the app didn't supply it (or supplied no fields). var clientInfo = this.options.getClientInfo(); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java index ebb54db6db..ee9d97e1e9 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java @@ -9,19 +9,19 @@ import com.fasterxml.jackson.annotation.JsonProperty; /** - * Identity of the integrating host, declared on the {@code server.connect} - * handshake. + * Identity of the integrating application, declared on the + * {@code server.connect} handshake. *

* Declaring it lets the telemetry the runtime emits on the connection be - * attributed to a single, consistent surface (the host editor and its Copilot - * extension) instead of the runtime's own build. All fields are optional; an + * attributed to a single, consistent surface (the application and its Copilot + * integration) instead of the runtime's own build. All fields are optional; an * empty field is omitted from the handshake. * *

Example Usage

* *
{@code
- * var options = new CopilotClientOptions().setClientInfo(new ClientInfo().setEditorName("vscode")
- * 		.setEditorVersion("1.124.2").setExtensionName("copilot-chat").setExtensionVersion("0.54.0"));
+ * var options = new CopilotClientOptions().setClientInfo(new ClientInfo().setApplicationName("acme-developer-portal")
+ * 		.setApplicationVersion("2.4.0").setIntegrationName("copilot-assistant").setIntegrationVersion("1.5.0"));
  * }
* * @see CopilotClientOptions#setClientInfo(ClientInfo) @@ -30,99 +30,105 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class ClientInfo { - @JsonProperty("editorName") - private String editorName; + private String applicationName; - @JsonProperty("editorVersion") - private String editorVersion; + private String applicationVersion; - @JsonProperty("extensionName") - private String extensionName; + private String integrationName; - @JsonProperty("extensionVersion") - private String extensionVersion; + private String integrationVersion; /** - * Gets the name of the host editor. + * Gets the name of the application using the SDK. * - * @return the editor name (e.g., {@code "vscode"}), or {@code null} + * @return the application name, or {@code null} */ - public String getEditorName() { - return editorName; + @JsonProperty("editorName") + public String getApplicationName() { + return applicationName; } /** - * Sets the name of the host editor. + * Sets the name of the application using the SDK. * - * @param editorName - * the editor name (e.g., {@code "vscode"}) + * @param applicationName + * the application name * @return this client info for method chaining */ - public ClientInfo setEditorName(String editorName) { - this.editorName = editorName; + @JsonProperty("editorName") + public ClientInfo setApplicationName(String applicationName) { + this.applicationName = applicationName; return this; } /** - * Gets the version of the host editor. + * Gets the version of the application using the SDK. * - * @return the editor version (e.g., {@code "1.124.2"}), or {@code null} + * @return the application version, or {@code null} */ - public String getEditorVersion() { - return editorVersion; + @JsonProperty("editorVersion") + public String getApplicationVersion() { + return applicationVersion; } /** - * Sets the version of the host editor. + * Sets the version of the application using the SDK. * - * @param editorVersion - * the editor version (e.g., {@code "1.124.2"}) + * @param applicationVersion + * the application version * @return this client info for method chaining */ - public ClientInfo setEditorVersion(String editorVersion) { - this.editorVersion = editorVersion; + @JsonProperty("editorVersion") + public ClientInfo setApplicationVersion(String applicationVersion) { + this.applicationVersion = applicationVersion; return this; } /** - * Gets the name of the Copilot extension within the host. + * Gets the optional name of a specific integration within the application, such + * as an extension or plugin. * - * @return the extension name (e.g., {@code "copilot-chat"}), or {@code null} + * @return the integration name, or {@code null} */ - public String getExtensionName() { - return extensionName; + @JsonProperty("extensionName") + public String getIntegrationName() { + return integrationName; } /** - * Sets the name of the Copilot extension within the host. + * Sets the optional name of a specific integration within the application, such + * as an extension or plugin. * - * @param extensionName - * the extension name (e.g., {@code "copilot-chat"}) + * @param integrationName + * the integration name * @return this client info for method chaining */ - public ClientInfo setExtensionName(String extensionName) { - this.extensionName = extensionName; + @JsonProperty("extensionName") + public ClientInfo setIntegrationName(String integrationName) { + this.integrationName = integrationName; return this; } /** - * Gets the version of the Copilot extension within the host. + * Gets the optional version of the named integration. * - * @return the extension version (e.g., {@code "0.54.0"}), or {@code null} + * @return the integration version, or {@code null} */ - public String getExtensionVersion() { - return extensionVersion; + @JsonProperty("extensionVersion") + public String getIntegrationVersion() { + return integrationVersion; } /** - * Sets the version of the Copilot extension within the host. + * Sets the optional version of the named integration. * - * @param extensionVersion - * the extension version (e.g., {@code "0.54.0"}) + * @param integrationVersion + * the integration version * @return this client info for method chaining */ - public ClientInfo setExtensionVersion(String extensionVersion) { - this.extensionVersion = extensionVersion; + @JsonProperty("extensionVersion") + public ClientInfo setIntegrationVersion(String integrationVersion) { + this.integrationVersion = integrationVersion; return this; } @@ -135,7 +141,8 @@ public ClientInfo setExtensionVersion(String extensionVersion) { */ @JsonIgnore public boolean isEmpty() { - return isBlank(editorName) && isBlank(editorVersion) && isBlank(extensionName) && isBlank(extensionVersion); + return isBlank(applicationName) && isBlank(applicationVersion) && isBlank(integrationName) + && isBlank(integrationVersion); } private static boolean isBlank(String value) { diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java index 3af76da52f..c67947bf48 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java @@ -680,7 +680,7 @@ public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) { } /** - * Gets the integrating host's declared identity. + * Gets the integrating application's declared identity. * * @return the client info, or {@code null} * @since 1.6.0 @@ -690,17 +690,17 @@ public ClientInfo getClientInfo() { } /** - * Declares the integrating host's identity, forwarded to the runtime on the - * {@code server.connect} handshake. + * Declares the integrating application's identity, forwarded to the runtime on + * the {@code server.connect} handshake. *

* Declaring it lets the telemetry the runtime emits on this connection be - * attributed to a consistent surface (the host editor and its Copilot - * extension) instead of the runtime's own build. All fields on + * attributed to a consistent surface (the application and its Copilot + * integration) instead of the runtime's own build. All fields on * {@link ClientInfo} are optional; leave this unset to keep the runtime's * default attribution. * * @param clientInfo - * the host identity to declare + * the application identity to declare * @return this options instance for method chaining * @since 1.6.0 */ diff --git a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java index af83fc7803..5a6e378b2a 100644 --- a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java +++ b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java @@ -209,17 +209,18 @@ void clientOmitsForwardingWhenNoHandler() throws Exception { void connectForwardsDeclaredClientInfo() throws Exception { try (var server = new FakeRuntimeServer(); var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) - .setClientInfo(new ClientInfo().setEditorName("JetBrains-IU").setEditorVersion("2026.1") - .setExtensionName("copilot-intellij").setExtensionVersion("1.5.0")))) { + .setClientInfo(new ClientInfo().setApplicationName("acme-developer-portal") + .setApplicationVersion("2.4.0").setIntegrationName("copilot-assistant") + .setIntegrationVersion("1.5.0")))) { client.start().get(15, TimeUnit.SECONDS); JsonNode connectParams = server.awaitConnect(); JsonNode clientInfo = connectParams.path("clientInfo"); assertEquals(4, clientInfo.size(), "clientInfo should carry only the four declared fields"); - assertEquals("JetBrains-IU", clientInfo.path("editorName").asText()); - assertEquals("2026.1", clientInfo.path("editorVersion").asText()); - assertEquals("copilot-intellij", clientInfo.path("extensionName").asText()); + assertEquals("acme-developer-portal", clientInfo.path("editorName").asText()); + assertEquals("2.4.0", clientInfo.path("editorVersion").asText()); + assertEquals("copilot-assistant", clientInfo.path("extensionName").asText()); assertEquals("1.5.0", clientInfo.path("extensionVersion").asText()); } } @@ -241,13 +242,13 @@ void connectOmitsClientInfoWhenUnset() throws Exception { void connectOmitsEmptyClientInfoFields() throws Exception { try (var server = new FakeRuntimeServer(); var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) - .setClientInfo(new ClientInfo().setEditorName("example-editor")))) { + .setClientInfo(new ClientInfo().setApplicationName("example-app")))) { client.start().get(15, TimeUnit.SECONDS); JsonNode connectParams = server.awaitConnect(); JsonNode clientInfo = connectParams.path("clientInfo"); - assertEquals("example-editor", clientInfo.path("editorName").asText()); + assertEquals("example-app", clientInfo.path("editorName").asText()); assertFalse(clientInfo.has("editorVersion"), "unset editorVersion should be omitted"); assertFalse(clientInfo.has("extensionName"), "unset extensionName should be omitted"); assertFalse(clientInfo.has("extensionVersion"), "unset extensionVersion should be omitted"); @@ -258,14 +259,14 @@ void connectOmitsEmptyClientInfoFields() throws Exception { void connectDropsEmptyClientInfoFields() throws Exception { try (var server = new FakeRuntimeServer(); var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) - .setClientInfo(new ClientInfo().setEditorName("vscode").setEditorVersion("")))) { + .setClientInfo(new ClientInfo().setApplicationName("example-app").setApplicationVersion("")))) { client.start().get(15, TimeUnit.SECONDS); JsonNode connectParams = server.awaitConnect(); JsonNode clientInfo = connectParams.path("clientInfo"); assertEquals(1, clientInfo.size(), "clientInfo should carry only the non-empty field"); - assertEquals("vscode", clientInfo.path("editorName").asText()); + assertEquals("example-app", clientInfo.path("editorName").asText()); assertFalse(clientInfo.has("editorVersion"), "empty editorVersion should be dropped"); } } @@ -274,8 +275,8 @@ void connectDropsEmptyClientInfoFields() throws Exception { void connectOmitsAllEmptyClientInfo() throws Exception { try (var server = new FakeRuntimeServer(); var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) - .setClientInfo(new ClientInfo().setEditorName("").setEditorVersion("").setExtensionName("") - .setExtensionVersion("")))) { + .setClientInfo(new ClientInfo().setApplicationName("").setApplicationVersion("") + .setIntegrationName("").setIntegrationVersion("")))) { client.start().get(15, TimeUnit.SECONDS); @@ -286,7 +287,7 @@ void connectOmitsAllEmptyClientInfo() throws Exception { @Test void optionsRetainAndCloneClientInfo() { - var info = new ClientInfo().setEditorName("example-editor"); + var info = new ClientInfo().setApplicationName("example-app"); var options = new CopilotClientOptions().setClientInfo(info); assertSame(info, options.getClientInfo()); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index eb8391e1dd..83b0a5f483 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -270,10 +270,10 @@ function toWireCustomAgents(agents: CustomAgentConfig[] | undefined): unknown[] function clientInfoToWire(info: CopilotClientInfo | undefined): ConnectClientInfo | undefined { if (info == null) return undefined; const wire: ConnectClientInfo = {}; - if (info.editorName) wire.editorName = info.editorName; - if (info.editorVersion) wire.editorVersion = info.editorVersion; - if (info.extensionName) wire.extensionName = info.extensionName; - if (info.extensionVersion) wire.extensionVersion = info.extensionVersion; + if (info.applicationName) wire.editorName = info.applicationName; + if (info.applicationVersion) wire.editorVersion = info.applicationVersion; + if (info.integrationName) wire.extensionName = info.integrationName; + if (info.integrationVersion) wire.extensionVersion = info.integrationVersion; return Object.keys(wire).length > 0 ? wire : undefined; } @@ -2247,7 +2247,7 @@ export class CopilotClient { if (this.onGitHubTelemetry != null) { connectParams.enableGitHubTelemetryForwarding = true; } - // Declare the integrating host's identity so the runtime attributes + // Declare the integrating application's identity so the runtime attributes // the telemetry it emits on this connection to a consistent surface // instead of its own build. Empty fields are dropped, and an // all-empty identity is omitted entirely. diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index efed11194a..128ced3d68 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -308,7 +308,7 @@ export type InternalRuntimeConnection = RuntimeConnection | ParentProcessRuntime export type CopilotClientMode = "empty" | "copilot-cli"; /** - * Identity of the integrating host, declared once on the `server.connect` + * Identity of the integrating application, declared once on the `server.connect` * handshake so the telemetry the runtime emits on this connection is attributed * to a single, consistent surface rather than to the runtime's own build. * @@ -318,24 +318,24 @@ export type CopilotClientMode = "empty" | "copilot-cli"; */ export interface CopilotClientInfo { /** - * Name of the host editor, e.g. `"vscode"`. + * Name of the application using the SDK, e.g. `"acme-developer-portal"`. */ - editorName?: string; + applicationName?: string; /** - * Version of the host editor, e.g. `"1.124.2"`. + * Version of the application using the SDK, e.g. `"2.4.0"`. */ - editorVersion?: string; + applicationVersion?: string; /** - * Name of the Copilot extension within the host, e.g. `"copilot-chat"`. + * Optional name of a specific integration within the application, such as an extension or plugin. */ - extensionName?: string; + integrationName?: string; /** - * Version of the Copilot extension within the host, e.g. `"0.54.0"`. + * Optional version of the integration identified by `integrationName`. */ - extensionVersion?: string; + integrationVersion?: string; } export interface CopilotClientOptions { @@ -510,11 +510,11 @@ export interface CopilotClientOptions { enableRemoteSessions?: boolean; /** - * Identity of the integrating host, forwarded to the runtime on the + * Identity of the integrating application, forwarded to the runtime on the * `server.connect` handshake. Declaring it lets the telemetry the runtime * emits on this connection be attributed to a single, consistent surface - * (e.g. the host editor and its Copilot extension) instead of the runtime's - * own build. All fields are optional; omit it to keep the default + * (e.g. the application and its Copilot integration) instead of the + * runtime's own build. All fields are optional; omit it to keep the default * attribution. */ clientInfo?: CopilotClientInfo; diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index cef9cbf490..bd1cf9812f 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -4272,15 +4272,20 @@ describe("connect handshake clientInfo", () => { it("forwards a declared client identity on the connect handshake", async () => { const clientInfo = { - editorName: "JetBrains-IU", - editorVersion: "2026.1", - extensionName: "copilot-intellij", - extensionVersion: "1.5.0", + applicationName: "acme-developer-portal", + applicationVersion: "2.4.0", + integrationName: "copilot-assistant", + integrationVersion: "1.5.0", }; const params = await captureConnectParams({ clientInfo }); - expect(params.clientInfo).toEqual(clientInfo); + expect(params.clientInfo).toEqual({ + editorName: "acme-developer-portal", + editorVersion: "2.4.0", + extensionName: "copilot-assistant", + extensionVersion: "1.5.0", + }); }); it("omits clientInfo from the handshake when the host declares none", async () => { @@ -4292,28 +4297,28 @@ describe("connect handshake clientInfo", () => { it("drops empty fields and omits an all-empty identity", async () => { const allEmpty = await captureConnectParams({ clientInfo: { - editorName: "", - editorVersion: "", - extensionName: "", - extensionVersion: "", + applicationName: "", + applicationVersion: "", + integrationName: "", + integrationVersion: "", }, }); expect(allEmpty).not.toHaveProperty("clientInfo"); const partial = await captureConnectParams({ - clientInfo: { editorName: "vscode", editorVersion: "" }, + clientInfo: { applicationName: "example-app", applicationVersion: "" }, }); - expect(partial.clientInfo).toEqual({ editorName: "vscode" }); + expect(partial.clientInfo).toEqual({ editorName: "example-app" }); }); it("keeps telemetry forwarding alongside a declared identity", async () => { const params = await captureConnectParams({ - clientInfo: { editorName: "example-editor" }, + clientInfo: { applicationName: "example-app" }, onGitHubTelemetry: () => {}, }); expect(params).toMatchObject({ - clientInfo: { editorName: "example-editor" }, + clientInfo: { editorName: "example-app" }, enableGitHubTelemetryForwarding: true, }); }); diff --git a/python/copilot/client.py b/python/copilot/client.py index 4db29d1e78..929194c9b7 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -508,22 +508,22 @@ class TelemetryConfig(TypedDict, total=False): class ClientInfo(TypedDict, total=False): - """Identity of the integrating host, declared on the ``server.connect`` handshake. + """Identity of the integrating application, declared on ``server.connect``. Declaring it lets the telemetry the runtime emits on this connection be - attributed to a single, consistent surface (the host editor and its Copilot - extension) instead of the runtime's own build. All fields are optional; omit - any of them (or the whole object) to keep the runtime's default attribution. + attributed to a single, consistent surface (the application and its Copilot + integration) instead of the runtime's own build. All fields are optional; + omit any of them (or the whole object) to keep the default attribution. """ - editor_name: str - """Name of the host editor, e.g. ``"vscode"``.""" - editor_version: str - """Version of the host editor, e.g. ``"1.124.2"``.""" - extension_name: str - """Name of the Copilot extension within the host, e.g. ``"copilot-chat"``.""" - extension_version: str - """Version of the Copilot extension within the host, e.g. ``"0.54.0"``.""" + application_name: str + """Name of the application using the SDK, e.g. ``"acme-developer-portal"``.""" + application_version: str + """Version of the application using the SDK, e.g. ``"2.4.0"``.""" + integration_name: str + """Optional name of an application integration, such as an extension or plugin.""" + integration_version: str + """Optional version of the integration named by ``integration_name``.""" def _client_info_to_wire(client_info: ClientInfo | None) -> dict[str, str] | None: @@ -536,14 +536,14 @@ def _client_info_to_wire(client_info: ClientInfo | None) -> dict[str, str] | Non if not client_info: return None wire: dict[str, str] = {} - if client_info.get("editor_name"): - wire["editorName"] = client_info["editor_name"] - if client_info.get("editor_version"): - wire["editorVersion"] = client_info["editor_version"] - if client_info.get("extension_name"): - wire["extensionName"] = client_info["extension_name"] - if client_info.get("extension_version"): - wire["extensionVersion"] = client_info["extension_version"] + if client_info.get("application_name"): + wire["editorName"] = client_info["application_name"] + if client_info.get("application_version"): + wire["editorVersion"] = client_info["application_version"] + if client_info.get("integration_name"): + wire["extensionName"] = client_info["integration_name"] + if client_info.get("integration_version"): + wire["extensionVersion"] = client_info["integration_version"] return wire or None @@ -1621,7 +1621,7 @@ def __init__( Control integration). When ``True``, sessions in a GitHub repository working directory are accessible from GitHub web and mobile. - client_info: Identity of the integrating host, forwarded to the + client_info: Identity of the integrating application, forwarded to the runtime on the ``server.connect`` handshake. Declaring it lets the telemetry the runtime emits on this connection be attributed to a consistent surface instead of the runtime's own build. All @@ -4102,7 +4102,7 @@ async def _verify_protocol_version(self) -> None: # event is forwarded). Also sent on session.create/resume for older CLIs. if self._on_github_telemetry is not None: connect_params["enableGitHubTelemetryForwarding"] = True - # Declare the integrating host's identity so the runtime attributes + # Declare the integrating application's identity so the runtime attributes # the telemetry it emits on this connection to a consistent surface # instead of its own build. Omitted when the app didn't supply it. client_info = _client_info_to_wire(self._options.client_info) diff --git a/python/test_client.py b/python/test_client.py index 197103dd68..bf8de113ae 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -3156,10 +3156,10 @@ async def test_connect_forwards_client_info(self): client = CopilotClient( connection=RuntimeConnection.for_stdio(path=CLI_PATH), client_info={ - "editor_name": "JetBrains-IU", - "editor_version": "2026.1", - "extension_name": "copilot-intellij", - "extension_version": "1.5.0", + "application_name": "acme-developer-portal", + "application_version": "2.4.0", + "integration_name": "copilot-assistant", + "integration_version": "1.5.0", }, ) captured = {} @@ -3172,9 +3172,9 @@ async def request(self, method, params, **kwargs): client._client = _FakeClient() await client._verify_protocol_version() assert captured["connect"]["clientInfo"] == { - "editorName": "JetBrains-IU", - "editorVersion": "2026.1", - "extensionName": "copilot-intellij", + "editorName": "acme-developer-portal", + "editorVersion": "2.4.0", + "extensionName": "copilot-assistant", "extensionVersion": "1.5.0", } @@ -3196,7 +3196,7 @@ async def request(self, method, params, **kwargs): async def test_connect_forwards_partial_client_info_with_forwarding(self): client = CopilotClient( connection=RuntimeConnection.for_stdio(path=CLI_PATH), - client_info={"editor_name": "example-editor"}, + client_info={"application_name": "example-app"}, on_github_telemetry=lambda _notification: None, ) captured = {} @@ -3208,14 +3208,14 @@ async def request(self, method, params, **kwargs): client._client = _FakeClient() await client._verify_protocol_version() - assert captured["connect"]["clientInfo"] == {"editorName": "example-editor"} + assert captured["connect"]["clientInfo"] == {"editorName": "example-app"} assert captured["connect"]["enableGitHubTelemetryForwarding"] is True @pytest.mark.asyncio async def test_connect_drops_empty_client_info_fields(self): client = CopilotClient( connection=RuntimeConnection.for_stdio(path=CLI_PATH), - client_info={"editor_name": "vscode", "editor_version": ""}, + client_info={"application_name": "example-app", "application_version": ""}, ) captured = {} @@ -3226,17 +3226,17 @@ async def request(self, method, params, **kwargs): client._client = _FakeClient() await client._verify_protocol_version() - assert captured["connect"]["clientInfo"] == {"editorName": "vscode"} + assert captured["connect"]["clientInfo"] == {"editorName": "example-app"} @pytest.mark.asyncio async def test_connect_omits_all_empty_client_info(self): client = CopilotClient( connection=RuntimeConnection.for_stdio(path=CLI_PATH), client_info={ - "editor_name": "", - "editor_version": "", - "extension_name": "", - "extension_version": "", + "application_name": "", + "application_version": "", + "integration_name": "", + "integration_version": "", }, ) captured = {} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 9734417e20..fd7f12cf14 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -401,16 +401,16 @@ pub struct ClientOptions { /// (the default) or are stripped to a minimal/safe baseline. See /// [`ClientMode`] for the contract and trade-offs. pub mode: ClientMode, - /// Declares the integrating host's identity, forwarded to the runtime on + /// Declares the integrating application's identity, forwarded to the runtime on /// the `server.connect` handshake. Declaring it lets the telemetry the /// runtime emits on this connection be attributed to a consistent surface - /// (the host editor and its Copilot extension) instead of the runtime's own + /// (the application and its Copilot integration) instead of the runtime's own /// build. All fields are optional; leave it `None` to keep the runtime's /// default attribution. pub client_info: Option, } -/// Identity of the integrating host, declared on the `server.connect` +/// Identity of the integrating application, declared on the `server.connect` /// handshake. /// /// Declaring it lets the telemetry the runtime emits on the connection be @@ -424,14 +424,15 @@ pub struct ClientOptions { #[derive(Debug, Clone, Default, PartialEq, Eq)] #[non_exhaustive] pub struct ClientInfo { - /// Name of the host editor, e.g. `"vscode"`. - pub editor_name: Option, - /// Version of the host editor, e.g. `"1.124.2"`. - pub editor_version: Option, - /// Name of the Copilot extension within the host, e.g. `"copilot-chat"`. - pub extension_name: Option, - /// Version of the Copilot extension within the host, e.g. `"0.54.0"`. - pub extension_version: Option, + /// Name of the application using the SDK. + pub application_name: Option, + /// Version of the application using the SDK. + pub application_version: Option, + /// Optional name of a specific integration within the application, such as an + /// extension or plugin. + pub integration_name: Option, + /// Optional version of the integration identified by [`Self::integration_name`]. + pub integration_version: Option, } impl ClientInfo { @@ -441,27 +442,29 @@ impl ClientInfo { Self::default() } - /// Set the host editor name, e.g. `"vscode"`. - pub fn with_editor_name(mut self, editor_name: impl Into) -> Self { - self.editor_name = Some(editor_name.into()); + /// Set the name of the application using the SDK. + pub fn with_application_name(mut self, application_name: impl Into) -> Self { + self.application_name = Some(application_name.into()); self } - /// Set the host editor version, e.g. `"1.124.2"`. - pub fn with_editor_version(mut self, editor_version: impl Into) -> Self { - self.editor_version = Some(editor_version.into()); + /// Set the version of the application using the SDK. + pub fn with_application_version(mut self, application_version: impl Into) -> Self { + self.application_version = Some(application_version.into()); self } - /// Set the Copilot extension name within the host, e.g. `"copilot-chat"`. - pub fn with_extension_name(mut self, extension_name: impl Into) -> Self { - self.extension_name = Some(extension_name.into()); + /// Set the name of a specific integration within the application, such as an + /// extension or plugin. + pub fn with_integration_name(mut self, integration_name: impl Into) -> Self { + self.integration_name = Some(integration_name.into()); self } - /// Set the Copilot extension version within the host, e.g. `"0.54.0"`. - pub fn with_extension_version(mut self, extension_version: impl Into) -> Self { - self.extension_version = Some(extension_version.into()); + /// Set the version of the integration identified by + /// [`Self::with_integration_name`]. + pub fn with_integration_version(mut self, integration_version: impl Into) -> Self { + self.integration_version = Some(integration_version.into()); self } @@ -469,10 +472,10 @@ impl ClientInfo { /// SDK omits `clientInfo` from the handshake and the runtime keeps its /// default attribution. fn is_empty(&self) -> bool { - Self::non_empty(&self.editor_name).is_none() - && Self::non_empty(&self.editor_version).is_none() - && Self::non_empty(&self.extension_name).is_none() - && Self::non_empty(&self.extension_version).is_none() + Self::non_empty(&self.application_name).is_none() + && Self::non_empty(&self.application_version).is_none() + && Self::non_empty(&self.integration_name).is_none() + && Self::non_empty(&self.integration_version).is_none() } /// Clone the field only when it holds a non-empty string, so empty fields are @@ -488,10 +491,10 @@ impl ClientInfo { return None; } Some(crate::generated::api_types::ConnectClientInfo { - editor_name: Self::non_empty(&self.editor_name), - editor_version: Self::non_empty(&self.editor_version), - extension_name: Self::non_empty(&self.extension_name), - extension_version: Self::non_empty(&self.extension_version), + editor_name: Self::non_empty(&self.application_name), + editor_version: Self::non_empty(&self.application_version), + extension_name: Self::non_empty(&self.integration_name), + extension_version: Self::non_empty(&self.integration_version), }) } } @@ -1027,7 +1030,7 @@ impl ClientOptions { self } - /// Declare the integrating host's identity, forwarded to the runtime on + /// Declare the integrating application's identity, forwarded to the runtime on /// the `server.connect` handshake so its telemetry is attributed to a /// consistent surface. See [`Self::client_info`]. pub fn with_client_info(mut self, client_info: ClientInfo) -> Self { @@ -1203,7 +1206,7 @@ struct ClientInner { /// `None` for stdio and for external-server transport without an /// explicit token. effective_connection_token: Option, - /// Host identity forwarded on the `connect` handshake, set from + /// Application identity forwarded on the `connect` handshake, set from /// [`ClientOptions::client_info`]. `None` keeps the runtime's default /// telemetry attribution. client_info: Option, @@ -1822,7 +1825,7 @@ impl Client { /// Construct a [`Client`] from raw streams with a preset /// [`ClientInfo`], for integration testing the `connect` handshake's - /// host-identity forwarding path. + /// application-identity forwarding path. #[doc(hidden)] #[cfg(any(test, feature = "test-support"))] pub fn from_streams_with_client_info( @@ -2474,7 +2477,7 @@ impl Client { .on_github_telemetry .is_some() .then_some(true), - // Declare the integrating host's identity so the runtime attributes + // Declare the integrating application's identity so the runtime attributes // the telemetry it emits on this connection to a consistent surface // instead of its own build. `None` when the app didn't supply it, and // empty fields are dropped. diff --git a/rust/tests/protocol_version_test.rs b/rust/tests/protocol_version_test.rs index 33b6c8d155..cd8563f87b 100644 --- a/rust/tests/protocol_version_test.rs +++ b/rust/tests/protocol_version_test.rs @@ -240,10 +240,10 @@ async fn connect_handshake_forwards_auto_generated_token() { .unwrap(); } -/// Positive coverage for host-identity forwarding on the `connect` +/// Positive coverage for application-identity forwarding on the `connect` /// handshake. A client constructed with a [`ClientInfo`] MUST serialize it /// (camelCase) into the outbound `connect` request's `clientInfo` param so -/// the runtime attributes this connection's telemetry to the host surface. +/// the runtime attributes this connection's telemetry to the application. #[tokio::test] async fn connect_handshake_forwards_client_info() { let (client_write, server_read) = duplex(8192); @@ -254,10 +254,10 @@ async fn connect_handshake_forwards_client_info() { std::env::temp_dir(), Some( github_copilot_sdk::ClientInfo::new() - .with_editor_name("JetBrains-IU") - .with_editor_version("2026.1") - .with_extension_name("copilot-intellij") - .with_extension_version("1.5.0"), + .with_application_name("acme-developer-portal") + .with_application_version("2.4.0") + .with_integration_name("copilot-assistant") + .with_integration_version("1.5.0"), ), ) .unwrap(); @@ -273,9 +273,9 @@ async fn connect_handshake_forwards_client_info() { let req = read_framed(&mut server_read).await; assert_eq!(req["method"], "connect"); let client_info = &req["params"]["clientInfo"]; - assert_eq!(client_info["editorName"], "JetBrains-IU"); - assert_eq!(client_info["editorVersion"], "2026.1"); - assert_eq!(client_info["extensionName"], "copilot-intellij"); + assert_eq!(client_info["editorName"], "acme-developer-portal"); + assert_eq!(client_info["editorVersion"], "2.4.0"); + assert_eq!(client_info["extensionName"], "copilot-assistant"); assert_eq!(client_info["extensionVersion"], "1.5.0"); let response = serde_json::json!({ @@ -305,8 +305,8 @@ async fn connect_handshake_omits_empty_client_info_fields() { std::env::temp_dir(), Some( github_copilot_sdk::ClientInfo::new() - .with_editor_name("example-editor") - .with_editor_version(""), + .with_application_name("example-app") + .with_application_version(""), ), ) .unwrap(); @@ -322,7 +322,7 @@ async fn connect_handshake_omits_empty_client_info_fields() { let req = read_framed(&mut server_read).await; assert_eq!(req["method"], "connect"); let client_info = &req["params"]["clientInfo"]; - assert_eq!(client_info["editorName"], "example-editor"); + assert_eq!(client_info["editorName"], "example-app"); assert!(client_info.get("editorVersion").is_none()); assert!(client_info.get("extensionName").is_none()); assert!(client_info.get("extensionVersion").is_none()); @@ -353,10 +353,10 @@ async fn connect_handshake_omits_all_empty_client_info() { std::env::temp_dir(), Some( github_copilot_sdk::ClientInfo::new() - .with_editor_name("") - .with_editor_version("") - .with_extension_name("") - .with_extension_version(""), + .with_application_name("") + .with_application_version("") + .with_integration_name("") + .with_integration_version(""), ), ) .unwrap(); From 42a84e2873f64af63c8f4ec738fca915d3f8bab1 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Wed, 2 Sep 2026 06:57:15 -0700 Subject: [PATCH 27/60] Improve Rust permission confirmation reliability (#2470) Surface confirmation RPC failures with structured context and cancel unresolved acknowledgement waits during session teardown. Add deterministic coverage for error, write-failure, missing-response, teardown, and success paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- rust/src/session.rs | 49 +++-- rust/tests/session_test.rs | 379 ++++++++++++++++++++++++++++++++++++- 2 files changed, 413 insertions(+), 15 deletions(-) diff --git a/rust/src/session.rs b/rust/src/session.rs index b9d2173055..65c2aff7c4 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -1567,7 +1567,7 @@ fn spawn_event_loop( _ = shutdown.cancelled() => break, Some(notification) = notifications.recv() => { handle_notification( - &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx, + &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx, &shutdown, ).await; } Some(request) = requests.recv() => { @@ -1719,6 +1719,7 @@ async fn handle_notification( capabilities: &Arc>, open_canvases: &Arc>>, event_tx: &tokio::sync::broadcast::Sender, + shutdown: &CancellationToken, ) { let dispatch_start = Instant::now(); let event = notification.event.clone(); @@ -1856,6 +1857,7 @@ async fn handle_notification( }; let client = client.clone(); let sid = session_id.clone(); + let shutdown = shutdown.clone(); let data = permission_request_data( ¬ification.event.data, handlers.managed_settings_enabled, @@ -1885,18 +1887,39 @@ async fn handle_notification( return; }; let rpc_start = Instant::now(); - let _ = client - .call( - rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST, - Some(params), - ) - .await; - tracing::debug!( - elapsed_ms = rpc_start.elapsed().as_millis(), - session_id = %sid, - request_id = %request_id, - "Session::handle_notification response sent successfully" - ); + let method = + rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST; + tokio::select! { + biased; + response = client.call(method, Some(params)) => { + match response { + Ok(_) => tracing::debug!( + elapsed_ms = rpc_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + method, + "Session::handle_notification response sent successfully" + ), + Err(error) => warn!( + error = %error, + session_id = %sid, + request_id = %request_id, + method, + "failed to deliver permission decision back to the runtime" + ), + } + } + _ = shutdown.cancelled() => { + warn!( + elapsed_ms = rpc_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + method, + delivery_outcome = "unknown", + "permission confirmation acknowledgement wait cancelled during session shutdown" + ); + } + } } .instrument(span), ); diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index fb16a0f674..9f86777ffc 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -1,8 +1,10 @@ #![allow(clippy::unwrap_used)] +use std::collections::HashMap; +use std::fmt; use std::path::Path; use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::time::Duration; use async_trait::async_trait; @@ -34,9 +36,151 @@ use github_copilot_sdk::types::{ use github_copilot_sdk::{Client, ContextTier, ErrorKind, ProtocolErrorKind, tool}; use serde_json::Value; use tokio::io::{AsyncWrite, AsyncWriteExt, duplex}; +use tokio::sync::Notify; use tokio::time::timeout; +use tracing::field::{Field, Visit}; +use tracing::span::{Attributes, Id, Record}; +use tracing::{Event, Metadata, Subscriber}; const TIMEOUT: Duration = Duration::from_secs(2); +const PERMISSION_CONFIRMATION_METHOD: &str = "session.permissions.handlePendingPermissionRequest"; + +#[derive(Clone, Debug)] +struct CapturedTraceEvent { + fields: HashMap, +} + +impl CapturedTraceEvent { + fn message_contains(&self, expected: &str) -> bool { + self.fields + .get("message") + .is_some_and(|message| message.contains(expected)) + } + + fn field_is(&self, name: &str, expected: &str) -> bool { + self.fields.get(name).is_some_and(|value| value == expected) + } +} + +#[derive(Clone, Default)] +struct TraceCapture { + events: Arc>>, +} + +impl TraceCapture { + fn permission_outcome(&self, request_id: &str) -> Option { + self.events + .lock() + .unwrap() + .iter() + .find(|event| { + event.field_is("request_id", request_id) + && (event.message_contains( + "Session::handle_notification response sent successfully", + ) || event.message_contains( + "failed to deliver permission decision back to the runtime", + ) || event + .message_contains("permission confirmation acknowledgement wait cancelled")) + }) + .cloned() + } + + async fn wait_for_permission_outcome(&self, request_id: &str) -> CapturedTraceEvent { + timeout(TIMEOUT, async { + loop { + if let Some(event) = self.permission_outcome(request_id) { + return event; + } + tokio::task::yield_now().await; + } + }) + .await + .expect("timed out waiting for permission confirmation diagnostic") + } +} + +#[derive(Default)] +struct TraceFieldVisitor { + fields: HashMap, +} + +impl Visit for TraceFieldVisitor { + fn record_bool(&mut self, field: &Field, value: bool) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_i64(&mut self, field: &Field, value: i64) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_u64(&mut self, field: &Field, value: u64) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_str(&mut self, field: &Field, value: &str) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { + self.fields + .insert(field.name().to_string(), format!("{value:?}")); + } +} + +struct CaptureSubscriber { + capture: TraceCapture, + next_span_id: AtomicU64, +} + +impl CaptureSubscriber { + fn new(capture: TraceCapture) -> Self { + Self { + capture, + next_span_id: AtomicU64::new(1), + } + } +} + +impl Subscriber for CaptureSubscriber { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { + true + } + + fn new_span(&self, _span: &Attributes<'_>) -> Id { + Id::from_u64(self.next_span_id.fetch_add(1, Ordering::Relaxed)) + } + + fn record(&self, _span: &Id, _values: &Record<'_>) {} + + fn record_follows_from(&self, _span: &Id, _follows: &Id) {} + + fn event(&self, event: &Event<'_>) { + let mut visitor = TraceFieldVisitor::default(); + event.record(&mut visitor); + self.capture + .events + .lock() + .unwrap() + .push(CapturedTraceEvent { + fields: visitor.fields, + }); + } + + fn enter(&self, _span: &Id) {} + + fn exit(&self, _span: &Id) {} +} + +fn capture_traces() -> (TraceCapture, tracing::dispatcher::DefaultGuard) { + let capture = TraceCapture::default(); + let dispatch = tracing::Dispatch::new(CaptureSubscriber::new(capture.clone())); + let guard = tracing::dispatcher::set_default(&dispatch); + (capture, guard) +} struct TestCanvasHandler; @@ -44,6 +188,25 @@ struct CancelMcpAuthHandler; struct ContextualApproveHandler; +struct GatedApproveHandler { + entered: Arc, + release: Arc, +} + +#[async_trait] +impl PermissionHandler for GatedApproveHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _data: github_copilot_sdk::PermissionRequestData, + ) -> PermissionResult { + self.entered.notify_one(); + self.release.notified().await; + PermissionResult::approve_once() + } +} + #[async_trait] impl PermissionHandler for ContextualApproveHandler { async fn handle( @@ -158,6 +321,16 @@ impl FakeServer { write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; } + async fn respond_error(&mut self, request: &Value, code: i64, message: &str) { + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "error": { "code": code, "message": message }, + }); + write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; + } + async fn send_notification(&mut self, method: &str, params: Value) { let notification = serde_json::json!({ "jsonrpc": "2.0", @@ -2792,7 +2965,8 @@ async fn user_input_requested_notification_does_not_double_dispatch() { } #[tokio::test] -async fn approve_all_handler_approves_permission() { +async fn permission_confirmation_success_behavior_is_unchanged() { + let (capture, _guard) = capture_traces(); let (_session, mut server) = create_session_pair_with_config(|cfg| { cfg.with_permission_handler(Arc::new(ApproveAllHandler)) }) @@ -2816,6 +2990,207 @@ async fn approve_all_handler_approves_permission() { ); assert_eq!(request["params"]["requestId"], "perm-auto"); assert_eq!(request["params"]["result"]["kind"], "approve-once"); + server.respond(&request, serde_json::json!({})).await; + + let outcome = capture.wait_for_permission_outcome("perm-auto").await; + assert!(outcome.message_contains("Session::handle_notification response sent successfully")); + assert!(outcome.field_is("session_id", &server.session_id)); + assert!(outcome.field_is("request_id", "perm-auto")); +} + +#[tokio::test] +async fn permission_confirmation_json_rpc_error_is_observable_and_connection_stays_responsive() { + let (capture, _guard) = capture_traces(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + let session = Arc::new(session); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-rpc-error", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + + let confirmation = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(confirmation["method"], PERMISSION_CONFIRMATION_METHOD); + server + .respond_error(&confirmation, -32603, "permission response rejected") + .await; + + let get_events = tokio::spawn({ + let session = session.clone(); + async move { session.get_events().await } + }); + let follow_up = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(follow_up["method"], "session.getMessages"); + server + .respond(&follow_up, serde_json::json!({ "events": [] })) + .await; + assert!(timeout(TIMEOUT, get_events).await.unwrap().unwrap().is_ok()); + + let outcome = capture.wait_for_permission_outcome("perm-rpc-error").await; + assert!(outcome.message_contains("failed to deliver permission decision back to the runtime")); + assert!(outcome.field_is("session_id", &server.session_id)); + assert!(outcome.field_is("request_id", "perm-rpc-error")); + assert!(outcome.field_is("method", PERMISSION_CONFIRMATION_METHOD)); +} + +#[tokio::test] +async fn permission_confirmation_write_failure_is_observable_and_events_stay_responsive() { + let (capture, _guard) = capture_traces(); + let entered = Arc::new(Notify::new()); + let release = Arc::new(Notify::new()); + let handler = Arc::new(GatedApproveHandler { + entered: entered.clone(), + release: release.clone(), + }); + let (session, mut server) = + create_session_pair_with_config(move |cfg| cfg.with_permission_handler(handler)).await; + let mut subscription = session.subscribe(); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-write-error", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + let permission_event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(permission_event.event_type, "permission.requested"); + timeout(TIMEOUT, entered.notified()).await.unwrap(); + + let FakeServer { + read, + mut write, + session_id, + } = server; + drop(read); + release.notify_one(); + + let idle_event = serde_json::json!({ + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": session_id, + "event": { + "id": "evt-after-write-error", + "timestamp": "2025-01-01T00:00:00Z", + "type": "session.idle", + "data": {}, + }, + }, + }); + write_framed(&mut write, &serde_json::to_vec(&idle_event).unwrap()).await; + let event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(event.event_type, "session.idle"); + + let outcome = capture + .wait_for_permission_outcome("perm-write-error") + .await; + assert!(outcome.message_contains("failed to deliver permission decision back to the runtime")); + assert!(outcome.field_is("session_id", &session_id)); + assert!(outcome.field_is("request_id", "perm-write-error")); + assert!(outcome.field_is("method", PERMISSION_CONFIRMATION_METHOD)); +} + +#[tokio::test] +async fn permission_confirmation_without_response_does_not_block_events_or_other_rpcs() { + let (capture, _guard) = capture_traces(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + let session = Arc::new(session); + let mut subscription = session.subscribe(); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-no-response", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + let permission_event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(permission_event.event_type, "permission.requested"); + let confirmation = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(confirmation["method"], PERMISSION_CONFIRMATION_METHOD); + + server + .send_event("session.idle", serde_json::json!({})) + .await; + let event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(event.event_type, "session.idle"); + + let get_events = tokio::spawn({ + let session = session.clone(); + async move { session.get_events().await } + }); + let follow_up = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(follow_up["method"], "session.getMessages"); + server + .respond(&follow_up, serde_json::json!({ "events": [] })) + .await; + assert!(timeout(TIMEOUT, get_events).await.unwrap().unwrap().is_ok()); + assert!( + capture.permission_outcome("perm-no-response").is_none(), + "the confirmation task should still be waiting silently for its response" + ); +} + +#[tokio::test] +async fn permission_confirmation_wait_is_cancelled_on_session_teardown() { + let (capture, _guard) = capture_traces(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + let session_id = server.session_id.clone(); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-teardown", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + let confirmation = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(confirmation["method"], PERMISSION_CONFIRMATION_METHOD); + + drop(session); + + let outcome = capture.wait_for_permission_outcome("perm-teardown").await; + assert!(outcome.message_contains("permission confirmation acknowledgement wait cancelled")); + assert!(outcome.field_is("session_id", &session_id)); + assert!(outcome.field_is("request_id", "perm-teardown")); + assert!(outcome.field_is("method", PERMISSION_CONFIRMATION_METHOD)); } #[tokio::test] From 1cc72da6ffee11ace68cde03441d6b41ffa72fce Mon Sep 17 00:00:00 2001 From: Derek Legenzoff Date: Wed, 2 Sep 2026 06:57:35 -0700 Subject: [PATCH 28/60] test: update replay harness default to Claude Sonnet 5 (#2472) * test: update replay harness default to Claude Sonnet 5 Preserve explicit legacy-model replay while advertising a supported default in historical catalogs. Update synthetic C# catalogs, model-list expectations, and the model-specific rate-limit fixture. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: preserve legacy selection in catalog and vision fixtures Advertise Sonnet 4.5 alongside the current default when no capture exists, and explicitly pin capability-toggle tests to their existing model so they do not switch model families mid-test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: use Sonnet 5 for both vision capability turns Migrate session creation and capability updates together across Node, Python, Go, and C#. Update the shared fixture catalogs without changing conversation captures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79d231f3-b08c-460b-89ec-c2abbd554368 * test: migrate all Sonnet 4.5 fixtures to Sonnet 5 Use Sonnet 5 consistently in test sessions, fake providers, assertions, and replay catalogs. Remove the legacy catalog compatibility shim and retain model-independent replay coverage using an arbitrary provider model. Conversation captures are not regenerated. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79d231f3-b08c-460b-89ec-c2abbd554368 * test: fix migration formatting and fixture-authoring examples Apply Ruff and Spotless reflow after model identifiers shortened, and update Java snapshot-authoring templates to Sonnet 5 so new tests do not reintroduce the retired model. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79d231f3-b08c-460b-89ec-c2abbd554368 * test: trim replay coverage to the Sonnet 5 default Keep missing and empty catalog default coverage and the existing cached catalog test; remove tests added for the deleted compatibility shim. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79d231f3-b08c-460b-89ec-c2abbd554368 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79d231f3-b08c-460b-89ec-c2abbd554368 --- .../new-java-e2e-test-yaml-and-test/SKILL.md | 2 +- .../examples.md | 4 +-- dotnet/test/E2E/ClientOptionsE2ETests.cs | 12 +++---- dotnet/test/E2E/CopilotRequestE2EProvider.cs | 14 ++++---- .../E2E/CopilotRequestSessionIdE2ETests.cs | 6 ++-- .../E2E/CopilotRequestWebSocketE2ETests.cs | 2 +- dotnet/test/E2E/RewindE2ETests.cs | 2 +- dotnet/test/E2E/RpcServerE2ETests.cs | 2 +- dotnet/test/E2E/RpcSessionStateE2ETests.cs | 22 ++++++------ .../test/E2E/RpcSessionStateExtrasE2ETests.cs | 8 ++--- dotnet/test/E2E/SessionConfigE2ETests.cs | 26 +++++++------- dotnet/test/E2E/SessionE2ETests.cs | 2 +- dotnet/test/Harness/E2ETestBackend.cs | 2 +- dotnet/test/Unit/E2ETestBackendTests.cs | 2 +- go/internal/e2e/client_options_e2e_test.go | 4 +-- .../e2e/copilot_request_helpers_test.go | 14 ++++---- .../copilot_request_session_id_e2e_test.go | 6 ++-- go/internal/e2e/rewind_e2e_test.go | 2 +- go/internal/e2e/rpc_e2e_test.go | 6 ++-- go/internal/e2e/rpc_server_e2e_test.go | 4 +-- go/internal/e2e/rpc_session_state_e2e_test.go | 20 +++++------ .../e2e/rpc_session_state_extras_e2e_test.go | 6 ++-- go/internal/e2e/session_config_e2e_test.go | 26 +++++++------- go/internal/e2e/session_e2e_test.go | 6 ++-- .../CopilotRequestSessionIdE2ETest.java | 6 ++-- .../copilot/CopilotRequestTestSupport.java | 14 ++++---- .../com/github/copilot/RpcServerE2ETest.java | 2 +- .../github/copilot/SessionConfigE2ETest.java | 12 +++---- .../java/com/github/copilot/e2e/RewindIT.java | 6 ++-- .../rpc/GeneratedRpcRecordsCoverageTest.java | 10 +++--- nodejs/test/e2e/client_options.e2e.test.ts | 12 +++---- .../copilot_request_cancel_error.e2e.test.ts | 6 ++-- .../e2e/copilot_request_handler.e2e.test.ts | 6 ++-- .../copilot_request_session_id.e2e.test.ts | 16 ++++----- .../test/e2e/disabled_mcp_servers.e2e.test.ts | 12 +++---- nodejs/test/e2e/rewind.e2e.test.ts | 2 +- nodejs/test/e2e/rpc.e2e.test.ts | 4 +-- nodejs/test/e2e/rpc_server.e2e.test.ts | 2 +- nodejs/test/e2e/rpc_session_state.e2e.test.ts | 18 +++++----- .../e2e/rpc_session_state_extras.e2e.test.ts | 6 ++-- nodejs/test/e2e/session.e2e.test.ts | 4 +-- nodejs/test/e2e/session_config.e2e.test.ts | 36 ++++++++++--------- python/e2e/_copilot_request_helpers.py | 14 ++++---- python/e2e/test_client_options_e2e.py | 12 +++---- .../test_copilot_request_session_id_e2e.py | 6 ++-- python/e2e/test_rewind_e2e.py | 2 +- python/e2e/test_rpc_e2e.py | 4 +-- python/e2e/test_rpc_server_e2e.py | 2 +- python/e2e/test_rpc_session_state_e2e.py | 18 +++++----- .../e2e/test_rpc_session_state_extras_e2e.py | 7 ++-- python/e2e/test_session_config_e2e.py | 24 +++++++------ python/e2e/test_session_e2e.py | 4 +-- rust/tests/e2e/client.rs | 2 +- rust/tests/e2e/client_options.rs | 4 +-- rust/tests/e2e/copilot_request_handler.rs | 16 ++++----- rust/tests/e2e/rewind.rs | 2 +- rust/tests/e2e/rpc_server.rs | 2 +- rust/tests/e2e/rpc_session_state.rs | 2 +- rust/tests/e2e/rpc_session_state_extras.rs | 2 +- rust/tests/e2e/session.rs | 2 +- rust/tests/e2e/session_config.rs | 20 +++++------ test/harness/modelProtocolAdapters.test.ts | 2 +- test/harness/replayingCapiProxy.test.ts | 36 +++++++++++++++++++ test/harness/replayingCapiProxy.ts | 2 +- .../should_abort_during_active_streaming.yaml | 2 +- ...ld_abort_during_active_tool_execution.yaml | 2 +- ...ompact_session_history_after_messages.yaml | 2 +- ..._block_sibling_tool_call_in_same_turn.yaml | 2 +- ...d_handle_freeform_user_input_response.yaml | 2 +- ...handler_when_model_uses_ask_user_tool.yaml | 2 +- ...receive_choices_in_user_input_request.yaml | 2 +- .../should_capture_exit_code_in_output.yaml | 2 +- .../should_capture_stderr_output.yaml | 2 +- .../should_create_a_new_file.yaml | 2 +- .../should_edit_a_file_successfully.yaml | 2 +- .../should_find_files_by_pattern.yaml | 2 +- ...ld_handle_nonexistent_file_gracefully.yaml | 2 +- .../should_read_file_with_line_range.yaml | 2 +- .../should_search_for_patterns_in_files.yaml | 2 +- ...nvas_list_discovers_declared_canvases.yaml | 2 +- ...models_withcustomhandler_callshandler.yaml | 2 +- .../client/should_force_stop_client.yaml | 2 +- .../should_get_authenticated_status.yaml | 2 +- test/snapshots/client/should_get_status.yaml | 2 +- ...should_list_models_when_authenticated.yaml | 2 +- ...ould_start_ping_and_stop_stdio_client.yaml | 2 +- ...should_start_ping_and_stop_tcp_client.yaml | 2 +- ...hould_stop_client_with_active_session.yaml | 2 +- .../should_delete_session_by_id.yaml | 2 +- ...last_session_id_after_session_created.yaml | 2 +- .../should_emit_session_lifecycle_events.yaml | 2 +- ..._deleted_lifecycle_event_when_deleted.yaml | 2 +- ...st_session_id_after_sending_a_message.yaml | 2 +- .../should_listen_on_configured_tcp_port.yaml | 2 +- ...ient_cwd_for_default_workingdirectory.yaml | 2 +- .../accept_mcp_servers_and_custom_agents.yaml | 2 +- ...on_with_commands_creates_successfully.yaml | 2 +- ...on_with_commands_resumes_successfully.yaml | 2 +- ...with_no_commands_creates_successfully.yaml | 2 +- ...vents_when_infinite_sessions_disabled.yaml | 2 +- ...on_with_low_threshold_and_emit_events.yaml | 2 +- .../accept_custom_agent_config_on_create.yaml | 2 +- .../accept_custom_agent_config_on_resume.yaml | 2 +- ...m_returns_false_when_handler_declines.yaml | 2 +- ...irm_returns_true_when_handler_accepts.yaml | 2 +- ...faults_capabilities_when_not_provided.yaml | 2 +- ...elicitation_returns_all_action_shapes.yaml | 2 +- ...ion_throws_when_capability_is_missing.yaml | 2 +- .../input_returns_freeform_value.yaml | 2 +- .../select_returns_selected_option.yaml | 2 +- ...uestelicitation_when_handler_provided.yaml | 2 +- ...icitationhandler_creates_successfully.yaml | 2 +- ..._capability_based_on_handler_presence.yaml | 2 +- ...emit_assistant_message_with_messageid.yaml | 2 +- ...ssistant_usage_event_after_model_call.yaml | 2 +- ...ect_order_for_tool_using_conversation.yaml | 2 +- ...fied_event_when_message_queue_changes.yaml | 2 +- ...ion_usage_info_event_after_model_call.yaml | 2 +- ..._execution_events_with_correct_fields.yaml | 2 +- ...ld_include_valid_fields_on_all_events.yaml | 2 +- ...e_order_in_getmessages_after_tool_use.yaml | 2 +- ...cution_when_pre_tool_use_returns_deny.yaml | 2 +- ...nvoke_both_hooks_for_single_tool_call.yaml | 2 +- ...tool_use_hook_after_model_runs_a_tool.yaml | 2 +- ..._tool_use_hook_when_model_runs_a_tool.yaml | 2 +- ...xecution_when_pretooluse_returns_deny.yaml | 2 +- ...ttooluse_hooks_for_a_single_tool_call.yaml | 2 +- ...osttooluse_hooks_for_single_tool_call.yaml | 2 +- ...ttooluse_hook_after_model_runs_a_tool.yaml | 2 +- ...retooluse_hook_when_model_runs_a_tool.yaml | 2 +- ..._posttooluse_to_return_modifiedresult.yaml | 2 +- ...eturn_modifiedargs_and_suppressoutput.yaml | 2 +- ...entstop_hook_and_apply_block_response.yaml | 2 +- ...nerroroccurred_hook_when_error_occurs.yaml | 2 +- ...end_hook_when_session_is_disconnected.yaml | 2 +- ...ke_onsessionstart_hook_on_new_session.yaml | 2 +- ...submitted_hook_when_sending_a_message.yaml | 2 +- ...sefailure_hook_for_failed_tool_result.yaml | 2 +- .../should_invoke_sessionend_hook.yaml | 2 +- .../should_invoke_sessionstart_hook.yaml | 4 +-- ...romptsubmitted_hook_and_modify_prompt.yaml | 2 +- ...ed_hook_and_modify_transformed_prompt.yaml | 2 +- .../should_register_erroroccurred_hook.yaml | 2 +- ...pt_both_mcp_servers_and_custom_agents.yaml | 2 +- ...agent_configuration_on_session_create.yaml | 2 +- ...agent_configuration_on_session_resume.yaml | 2 +- ...erver_configuration_on_session_create.yaml | 2 +- ...erver_configuration_on_session_resume.yaml | 2 +- .../accept_custom_agent_config_on_create.yaml | 2 +- .../accept_custom_agent_config_on_resume.yaml | 2 +- .../accept_mcp_server_config_on_create.yaml | 2 +- .../accept_mcp_server_config_on_resume.yaml | 2 +- ...accept_mcp_server_config_without_args.yaml | 2 +- .../accept_mcp_servers_and_custom_agents.yaml | 2 +- ...pt_both_mcp_servers_and_custom_agents.yaml | 2 +- ...agent_configuration_on_session_create.yaml | 2 +- ...agent_configuration_on_session_resume.yaml | 2 +- ...agent_configuration_on_session_resume.yaml | 2 +- ...erver_configuration_on_session_create.yaml | 2 +- ...erver_configuration_on_session_resume.yaml | 2 +- ...mcp_server_configuration_without_args.yaml | 2 +- ..._handle_custom_agent_with_mcp_servers.yaml | 2 +- ...custom_agent_with_tools_configuration.yaml | 2 +- .../should_handle_multiple_custom_agents.yaml | 2 +- .../should_handle_multiple_mcp_servers.yaml | 2 +- ...ide_excluded_tools_from_default_agent.yaml | 2 +- ...l_env_values_to_mcp_server_subprocess.yaml | 2 +- ...d_trip_mcp_server_elicitation_request.yaml | 2 +- .../accept_mcp_server_config_on_create.yaml | 2 +- .../accept_mcp_server_config_on_resume.yaml | 2 +- ...takes_effect_and_env_context_stripped.yaml | 2 +- ...iltin_star_exposes_all_built_in_tools.yaml | 2 +- ..._tools_subtracts_from_available_tools.yaml | 2 +- ...solated_set_shell_tool_is_not_exposed.yaml | 2 +- ...xt_from_the_system_message_by_default.yaml | 2 +- ...e_llm_follows_caller_content_verbatim.yaml | 2 +- ...mode_switch_handler_when_rate_limited.yaml | 4 +-- ...lan_mode_handler_when_model_uses_tool.yaml | 2 +- ...ee_tool_request_and_completion_events.yaml | 2 +- ...isconnecting_client_removes_its_tools.yaml | 2 +- ...es_permission_and_both_see_the_result.yaml | 2 +- ...ts_permission_and_both_see_the_result.yaml | 2 +- ...r_different_tools_and_agent_uses_both.yaml | 2 +- ...h_custom_agents_bound_to_their_models.yaml | 2 +- ...le_creation_then_reading_across_turns.yaml | 2 +- ..._use_tool_results_from_previous_turns.yaml | 2 +- ...g_external_tool_requests_after_resume.yaml | 2 +- ...ng_external_tool_request_after_resume.yaml | 2 +- ...nding_permission_request_after_resume.yaml | 2 +- ...ume_when_continuependingwork_is_false.yaml | 2 +- ...ume_when_continuependingwork_is_false.yaml | 2 +- ...tinuependingwork_true_in_resume_event.yaml | 2 +- ...cessfully_when_no_pending_work_exists.yaml | 2 +- ...atus_is_unauthenticated_without_token.yaml | 2 +- .../session_fails_with_invalid_token.yaml | 2 +- .../session_token_overrides_client_token.yaml | 2 +- ...ken_when_no_session_token_is_supplied.yaml | 2 +- .../permissions/async_permission_handler.yaml | 2 +- .../permissions/deny_permission.yaml | 2 +- .../permission_handler_errors.yaml | 2 +- ...permission_handler_for_shell_commands.yaml | 2 +- ...rmission_handler_for_write_operations.yaml | 2 +- ...esume_session_with_permission_handler.yaml | 2 +- ...ermission_when_handler_returns_denied.yaml | 2 +- ...ld_deny_permission_with_noresult_kind.yaml | 2 +- ...ations_when_handler_explicitly_denies.yaml | 2 +- ...andler_explicitly_denies_after_resume.yaml | 2 +- ...hould_handle_async_permission_handler.yaml | 2 +- ...rmission_requests_from_parallel_tools.yaml | 2 +- ..._permission_handler_errors_gracefully.yaml | 2 +- ...cision_annotated_with_decisioncontext.yaml | 2 +- ...rmission_handler_for_write_operations.yaml | 2 +- ...ive_toolcallid_in_permission_requests.yaml | 2 +- ...esume_session_with_permission_handler.yaml | 2 +- ..._handler_when_set_approve_all_enabled.yaml | 2 +- ...ould_wait_for_slow_permission_handler.yaml | 2 +- ...k_with_approve_all_permission_handler.yaml | 2 +- .../tool_call_id_in_permission_requests.yaml | 2 +- ...d_remove_meta_via_premcptoolcall_hook.yaml | 2 +- ..._replace_meta_via_premcptoolcall_hook.yaml | 2 +- ...ould_set_meta_via_premcptoolcall_hook.yaml | 2 +- ...rsisted_session_with_mcp_auth_handler.yaml | 2 +- ...restore_tracked_file_and_conversation.yaml | 2 +- ...ame_value_multiple_times_stays_stable.yaml | 2 +- .../name_set_with_unicode_round_trips.yaml | 2 +- ...on_approvals_on_fresh_session_is_noop.yaml | 2 +- ...ns_set_approve_all_toggle_round_trips.yaml | 2 +- ...delete_when_none_exists_is_idempotent.yaml | 2 +- ...empty_content_then_read_returns_empty.yaml | 2 +- ...ut_does_not_kill_long_running_command.yaml | 2 +- ..._on_fresh_session_returns_zero_tokens.yaml | 2 +- ...e_file_with_empty_content_round_trips.yaml | 2 +- ...e_file_with_large_content_round_trips.yaml | 2 +- ...file_with_unicode_content_round_trips.yaml | 2 +- ...tfiles_returns_sorted_or_stable_order.yaml | 2 +- ...ce_returns_stable_result_across_calls.yaml | 2 +- .../rpc_agents/should_call_agent_reload.yaml | 2 +- .../should_deselect_current_agent.yaml | 2 +- ...bagent_selected_and_deselected_events.yaml | 2 +- .../should_list_available_custom_agents.yaml | 2 +- ...list_when_no_custom_agents_configured.yaml | 2 +- ...return_null_when_no_agent_is_selected.yaml | 2 +- .../should_select_and_get_current_agent.yaml | 2 +- ...ould_allow_session_use_after_truncate.yaml | 2 +- ...emit_mode_changed_event_when_mode_set.yaml | 2 +- ...n_changed_event_for_update_and_delete.yaml | 2 +- ...ged_update_operation_on_second_update.yaml | 2 +- ...d_event_and_remove_events_on_truncate.yaml | 2 +- ...mit_title_changed_event_when_name_set.yaml | 2 +- ..._file_changed_event_when_file_created.yaml | 2 +- ...should_list_and_toggle_session_skills.yaml | 2 +- .../should_list_extensions.yaml | 2 +- ...st_mcp_servers_with_configured_server.yaml | 2 +- .../should_list_plugins.yaml | 2 +- .../should_reload_session_skills.yaml | 2 +- ...ror_when_extensions_are_not_available.yaml | 2 +- ...rror_when_mcp_host_is_not_initialized.yaml | 2 +- ...en_mcp_oauth_server_is_not_configured.yaml | 2 +- ...r_when_mcp_oauth_server_is_not_remote.yaml | 2 +- .../should_call_server_mcp_config_rpcs.yaml | 2 +- ..._round_trip_http_mcp_oauth_config_rpc.yaml | 2 +- .../should_configure_github_mcp_server.yaml | 2 +- ...t_running_status_for_connected_server.yaml | 2 +- ...should_reload_mcp_servers_with_config.yaml | 2 +- .../should_start_and_restart_mcp_server.yaml | 2 +- .../should_stop_running_mcp_server.yaml | 2 +- ..._listing_tools_for_unconnected_server.yaml | 2 +- ..._account_get_quota_when_authenticated.yaml | 2 +- ...all_rpc_models_list_with_typed_result.yaml | 2 +- ...rpc_ping_with_typed_params_and_result.yaml | 2 +- ...call_rpc_tools_list_with_typed_result.yaml | 2 +- ...should_discover_server_mcp_and_skills.yaml | 2 +- ...hould_get_set_and_clear_user_settings.yaml | 2 +- ...ist_getcurrentauth_and_logout_account.yaml | 2 +- ...chments_from_non_extension_connection.yaml | 2 +- .../should_reload_user_settings.yaml | 2 +- ...port_agent_registry_spawn_gate_closed.yaml | 2 +- ..._when_opening_session_without_context.yaml | 2 +- .../should_shut_down_owned_runtime.yaml | 2 +- ...enable_and_disable_marketplace_plugin.yaml | 2 +- ...local_plugin_with_deprecation_warning.yaml | 2 +- ...install_plugin_from_local_marketplace.yaml | 2 +- ..._refresh_and_remove_local_marketplace.yaml | 2 +- .../should_reload_mcp_config_cache.yaml | 2 +- .../should_update_all_installed_plugins.yaml | 2 +- ...ould_update_single_marketplace_plugin.yaml | 2 +- ...ng_remote_control_for_unknown_session.yaml | 2 +- ...ansfer_when_off_with_compare_and_swap.yaml | 2 +- ...ot_stopped_when_remote_control_is_off.yaml | 2 +- ...d_report_remote_control_status_as_off.yaml | 2 +- ..._treat_set_steering_as_no_op_when_off.yaml | 2 +- ...uld_call_session_rpc_model_getcurrent.yaml | 2 +- ...hould_call_session_rpc_model_switchto.yaml | 2 +- ...all_session_usage_and_permission_rpcs.yaml | 2 +- ...hould_call_workspace_file_rpc_methods.yaml | 2 +- ...ompact_session_history_after_messages.yaml | 2 +- ...e_with_nested_path_auto_creating_dirs.yaml | 2 +- ...ed_event_each_time_name_set_is_called.yaml | 2 +- ..._to_event_id_excluding_boundary_event.yaml | 2 +- ..._fork_session_with_persisted_messages.yaml | 2 +- .../should_get_and_set_session_metadata.yaml | 2 +- .../should_get_and_set_session_mode.yaml | 2 +- ...king_session_without_persisted_events.yaml | 2 +- .../should_read_update_and_delete_plan.yaml | 2 +- ...ject_empty_or_whitespace_session_name.yaml | 2 +- ..._reject_workspace_file_path_traversal.yaml | 2 +- ...or_reading_nonexistent_workspace_file.yaml | 2 +- ...n_forking_session_to_unknown_event_id.yaml | 2 +- ...ors_for_unsupported_session_rpc_paths.yaml | 2 +- ...eport_processing_and_context_metadata.yaml | 2 +- ...d_set_and_get_each_session_mode_value.yaml | 2 +- ..._workspace_file_with_update_operation.yaml | 2 +- ...dd_byok_provider_and_model_at_runtime.yaml | 2 +- ...ould_get_and_set_allowall_permissions.yaml | 2 +- ...tion_and_heaviest_messages_after_turn.yaml | 2 +- ...nt_tool_metadata_after_initialization.yaml | 2 +- .../should_get_telemetry_engagement_id.yaml | 2 +- .../should_list_models_for_session.yaml | 2 +- ...ead_empty_sql_todos_for_fresh_session.yaml | 2 +- .../should_reload_session_plugins.yaml | 2 +- ...uld_report_session_activity_when_idle.yaml | 2 +- ...ibility_as_unsynced_for_local_session.yaml | 2 +- ...tions_when_host_does_not_provide_them.yaml | 2 +- ...date_and_clear_live_subagent_settings.yaml | 2 +- .../should_execute_shell_command.yaml | 2 +- .../should_kill_shell_process.yaml | 2 +- ...t_fleet_and_complete_custom_tool_task.yaml | 2 +- ..._exec_with_custom_cwd_honors_override.yaml | 2 +- ...hell_exec_with_large_stdout_cleans_up.yaml | 2 +- ...mmand_returns_processid_and_cleans_up.yaml | 2 +- ...ell_exec_with_stderr_output_cleans_up.yaml | 2 +- ...th_timeout_kills_long_running_command.yaml | 2 +- ...ll_cleans_up_after_terminating_signal.yaml | 2 +- ..._kill_unknown_processid_returns_false.yaml | 2 +- ...d_cancel_user_requested_shell_command.yaml | 2 +- ..._execute_user_requested_shell_command.yaml | 2 +- ...urn_false_for_missing_task_operations.yaml | 2 +- ...ed_error_for_invalid_task_agent_model.yaml | 2 +- ...ted_error_for_missing_task_agent_type.yaml | 2 +- ...or_missing_pending_handler_requestids.yaml | 2 +- ...kground_agent_and_report_task_details.yaml | 2 +- .../should_answer_ephemeral_query.yaml | 2 +- ...ee_tool_request_and_completion_events.yaml | 2 +- ...isconnecting_client_removes_its_tools.yaml | 2 +- ...r_different_tools_and_agent_uses_both.yaml | 2 +- ...easync_from_handler_does_not_deadlock.yaml | 2 +- ...xception_does_not_halt_event_delivery.yaml | 2 +- ...en_an_mcp_oauth_handler_is_configured.yaml | 2 +- ...ely_while_events_stream_in_background.yaml | 2 +- ...e_and_returns_final_assistant_message.yaml | 2 +- .../sendandwait_throws_on_timeout.yaml | 2 +- ...anceledexception_when_token_cancelled.yaml | 2 +- .../session/should_abort_a_session.yaml | 2 +- .../should_accept_blob_attachments.yaml | 2 +- ...on_with_appended_systemmessage_config.yaml | 2 +- ..._create_a_session_with_availabletools.yaml | 2 +- ..._with_customized_systemmessage_config.yaml | 6 ++-- ...ssion_with_defaultagent_excludedtools.yaml | 2 +- ...d_create_a_session_with_excludedtools.yaml | 2 +- ...on_with_replaced_systemmessage_config.yaml | 2 +- ...create_session_with_custom_config_dir.yaml | 2 +- ...hould_create_session_with_custom_tool.yaml | 2 +- .../session/should_delete_session.yaml | 2 +- .../session/should_get_last_session_id.yaml | 2 +- .../session/should_get_session_metadata.yaml | 2 +- .../should_get_session_metadata_by_id.yaml | 2 +- .../should_have_stateful_conversation.yaml | 2 +- .../session/should_list_sessions.yaml | 2 +- .../should_list_sessions_with_context.yaml | 2 +- ...should_log_messages_at_various_levels.yaml | 2 +- .../should_receive_session_events.yaml | 2 +- ...d_resume_a_session_using_a_new_client.yaml | 2 +- ...esume_a_session_using_the_same_client.yaml | 2 +- ...hould_send_with_custom_requestheaders.yaml | 2 +- ...should_send_with_directory_attachment.yaml | 2 +- .../should_send_with_file_attachment.yaml | 2 +- ...send_with_github_reference_attachment.yaml | 2 +- .../should_send_with_mode_property.yaml | 2 +- ...should_send_with_selection_attachment.yaml | 2 +- .../should_set_model_on_existing_session.yaml | 2 +- ...should_set_model_with_reasoningeffort.yaml | 2 +- .../should_accept_blob_attachments.yaml | 2 +- .../should_accept_message_attachments.yaml | 2 +- ...oning_effort_values_on_session_create.yaml | 2 +- ...pply_availabletools_on_session_resume.yaml | 2 +- ...ly_excluded_built_in_agents_on_create.yaml | 2 +- ...ly_excluded_built_in_agents_on_resume.yaml | 2 +- ...pply_github_mcp_tool_config_on_create.yaml | 2 +- ...ply_instruction_directories_on_create.yaml | 2 +- ...ply_instruction_directories_on_resume.yaml | 2 +- ...pply_instructiondirectories_on_create.yaml | 2 +- ...pply_instructiondirectories_on_resume.yaml | 2 +- ...tructiondirectories_on_session_create.yaml | 2 +- ...tructiondirectories_on_session_resume.yaml | 2 +- ...ly_reasoning_effort_on_session_create.yaml | 2 +- ...should_apply_session_limits_on_create.yaml | 2 +- ...should_apply_session_limits_on_resume.yaml | 2 +- ...apply_systemmessage_on_session_resume.yaml | 2 +- ...ly_workingdirectory_on_session_resume.yaml | 2 +- ...e_session_with_custom_provider_config.yaml | 2 +- ...ould_forward_clientname_in_user_agent.yaml | 2 +- ...hould_forward_clientname_in_useragent.yaml | 2 +- ...ard_custom_provider_headers_on_create.yaml | 2 +- ...ard_custom_provider_headers_on_resume.yaml | 2 +- .../should_use_custom_session_id.yaml | 2 +- ...d_use_provider_model_id_as_wire_model.yaml | 2 +- ...e_workingdirectory_for_tool_execution.yaml | 2 +- ...on_disabled_then_enabled_via_setmodel.yaml | 2 +- ...on_enabled_then_disabled_via_setmodel.yaml | 2 +- ...ssion_data_from_fs_provider_on_resume.yaml | 2 +- ..._large_output_handling_into_sessionfs.yaml | 2 +- .../should_persist_plan_md_via_sessionfs.yaml | 2 +- ...tprovider_when_sessions_already_exist.yaml | 2 +- ...tions_through_the_session_fs_provider.yaml | 2 +- ...with_compaction_while_using_sessionfs.yaml | 2 +- ...rite_workspace_metadata_via_sessionfs.yaml | 2 +- ..._use_sql_tool_via_inherited_sessionfs.yaml | 2 +- ..._through_the_sessionfs_sqlite_handler.yaml | 2 +- .../should_delete_session_permanently.yaml | 2 +- ...te_events_between_concurrent_sessions.yaml | 2 +- ...ated_sessions_after_sending_a_message.yaml | 2 +- ...ts_via_getmessages_after_conversation.yaml | 2 +- ..._support_multiple_concurrent_sessions.yaml | 2 +- ...ged_and_exposes_rows_and_dependencies.yaml | 2 +- ...low_agent_with_skills_to_invoke_skill.yaml | 2 +- ...and_apply_skill_from_skilldirectories.yaml | 2 +- ...kill_when_disabled_via_disabledskills.yaml | 2 +- ..._skills_to_agent_without_skills_field.yaml | 2 +- ...before_deltas_with_matching_messageid.yaml | 2 +- ...ession_resume_with_streaming_disabled.yaml | 2 +- ...uce_deltas_when_streaming_is_disabled.yaml | 2 +- ...elta_events_when_streaming_is_enabled.yaml | 2 +- ...d_produce_deltas_after_session_resume.yaml | 2 +- ...ooluse_hooks_for_sub_agent_tool_calls.yaml | 2 +- ...d_continue_conversation_after_suspend.yaml | 2 +- ...ng_permission_request_when_suspending.yaml | 2 +- ...pending_external_tool_when_suspending.yaml | 2 +- ...suspend_idle_session_without_throwing.yaml | 2 +- ...replaced_identity_section_in_response.yaml | 2 +- ...replaced_preamble_section_in_response.yaml | 4 +-- ...form_modifications_to_section_content.yaml | 2 +- ...nsform_callbacks_with_section_content.yaml | 2 +- ...tic_overrides_and_transforms_together.yaml | 2 +- ...t_file_telemetry_for_sdk_interactions.yaml | 2 +- ...red_toolresultobject_from_custom_tool.yaml | 2 +- ...le_tool_result_with_denied_resulttype.yaml | 2 +- ...e_tool_result_with_failure_resulttype.yaml | 2 +- ..._tool_result_with_rejected_resulttype.yaml | 2 +- ...idated_zod_parameters_to_tool_handler.yaml | 2 +- ..._stringify_structured_results_for_llm.yaml | 2 +- .../can_receive_and_return_complex_types.yaml | 2 +- ...minal_tool_and_starts_the_seeded_turn.yaml | 2 +- ...es_custom_tool_when_permission_denied.yaml | 2 +- .../tools/ergonomic_tool_arity0.yaml | 2 +- .../tools/ergonomic_tool_arity2.yaml | 2 +- .../tools/ergonomic_tool_definition.yaml | 2 +- .../tools/handles_tool_calling_errors.yaml | 2 +- .../tools/invokes_built_in_tools.yaml | 2 +- test/snapshots/tools/invokes_custom_tool.yaml | 2 +- ...s_custom_tool_with_permission_handler.yaml | 2 +- .../tools/low_level_tool_definition.yaml | 2 +- ...rrides_built_in_tool_with_custom_tool.yaml | 2 +- ..._custom_tools_in_parallel_single_turn.yaml | 2 +- ...labletools_and_excludedtools_combined.yaml | 2 +- ...kippermission_sent_in_tool_definition.yaml | 2 +- 465 files changed, 705 insertions(+), 666 deletions(-) diff --git a/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md b/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md index d034b20377..a0fd94b943 100644 --- a/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md +++ b/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md @@ -33,7 +33,7 @@ The format is: ```yaml models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/.github/skills/new-java-e2e-test-yaml-and-test/examples.md b/.github/skills/new-java-e2e-test-yaml-and-test/examples.md index af82ef4dba..12971244a2 100644 --- a/.github/skills/new-java-e2e-test-yaml-and-test/examples.md +++ b/.github/skills/new-java-e2e-test-yaml-and-test/examples.md @@ -8,7 +8,7 @@ File: `test/snapshots/system_message_sections/should_use_replaced_identity_secti ```yaml models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -73,7 +73,7 @@ File: `test/snapshots/system_message_transform/should_invoke_transform_callbacks ```yaml models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: # First exchange: model decides to call tools - messages: diff --git a/dotnet/test/E2E/ClientOptionsE2ETests.cs b/dotnet/test/E2E/ClientOptionsE2ETests.cs index 5391e4bdbc..1ff473bd01 100644 --- a/dotnet/test/E2E/ClientOptionsE2ETests.cs +++ b/dotnet/test/E2E/ClientOptionsE2ETests.cs @@ -296,7 +296,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request var session = await Ctx.CreateSessionAsync(client, new SessionConfig { ClientName = "advanced-create-client", - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", ReasoningEffort = "medium", ReasoningSummary = ReasoningSummary.Detailed, ContextTier = ContextTier.LongContext, @@ -379,7 +379,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request Provider = "create-provider", Id = "create-model", Name = "Create Model", - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", WireModel = "create-wire-model", MaxContextWindowTokens = 12_000, MaxPromptTokens = 10_000, @@ -392,7 +392,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath)); var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create"); Assert.Equal("advanced-create-client", createRequest.GetProperty("clientName").GetString()); - Assert.Equal("claude-sonnet-4.5", createRequest.GetProperty("model").GetString()); + Assert.Equal("claude-sonnet-5", createRequest.GetProperty("model").GetString()); Assert.Equal("medium", createRequest.GetProperty("reasoningEffort").GetString()); Assert.Equal("detailed", createRequest.GetProperty("reasoningSummary").GetString()); Assert.Equal("long_context", createRequest.GetProperty("contextTier").GetString()); @@ -442,7 +442,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques var session = await Ctx.CreateSessionAsync(client, new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = new ProviderConfig { Type = "azure", @@ -453,7 +453,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques BearerToken = "provider-bearer-token", Azure = new AzureOptions { ApiVersion = "2024-02-15-preview" }, Headers = new Dictionary { ["X-Provider-Wire"] = "yes" }, - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", WireModel = "azure-deployment", MaxPromptTokens = 8192, MaxOutputTokens = 1024, @@ -471,7 +471,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques Assert.Equal("provider-bearer-token", provider.GetProperty("bearerToken").GetString()); Assert.Equal("2024-02-15-preview", provider.GetProperty("azure").GetProperty("apiVersion").GetString()); Assert.Equal("yes", provider.GetProperty("headers").GetProperty("X-Provider-Wire").GetString()); - Assert.Equal("claude-sonnet-4.5", provider.GetProperty("modelId").GetString()); + Assert.Equal("claude-sonnet-5", provider.GetProperty("modelId").GetString()); Assert.Equal("azure-deployment", provider.GetProperty("wireModel").GetString()); Assert.Equal(8192, provider.GetProperty("maxPromptTokens").GetInt32()); Assert.Equal(1024, provider.GetProperty("maxOutputTokens").GetInt32()); diff --git a/dotnet/test/E2E/CopilotRequestE2EProvider.cs b/dotnet/test/E2E/CopilotRequestE2EProvider.cs index 89826b4f84..f4c250752a 100644 --- a/dotnet/test/E2E/CopilotRequestE2EProvider.cs +++ b/dotnet/test/E2E/CopilotRequestE2EProvider.cs @@ -160,9 +160,9 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) private static readonly string[] ChatCompletionStreamEvents = [ - "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\n", - "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"" + SyntheticText + "\"},\"finish_reason\":null}]}\n\n", - "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}\n\n", + "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\n", + "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"" + SyntheticText + "\"},\"finish_reason\":null}]}\n\n", + "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}\n\n", "data: [DONE]\n\n", ]; @@ -172,7 +172,7 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) // runtime's Anthropic client fail with "stream ended without producing a Message". private static readonly string[] AnthropicStreamEvents = [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4.5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":1}}}\n\n", + "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":1}}}\n\n", "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n", "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"" + SyntheticText + "\"}}\n\n", "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n", @@ -184,13 +184,13 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) "{\"id\":\"resp_stub_1\",\"object\":\"response\",\"status\":\"completed\",\"output\":[{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"" + SyntheticText + "\"}]}],\"usage\":{\"input_tokens\":5,\"output_tokens\":7,\"total_tokens\":12}}"; private static readonly string BufferedChatCompletionJson = - "{\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"" + SyntheticText + "\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}"; + "{\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"" + SyntheticText + "\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}"; private static readonly string BufferedAnthropicMessageJson = - "{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4.5\",\"content\":[{\"type\":\"text\",\"text\":\"" + SyntheticText + "\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":7}}"; + "{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-5\",\"content\":[{\"type\":\"text\",\"text\":\"" + SyntheticText + "\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":7}}"; private const string ModelCatalogJson = - "{\"data\":[{\"id\":\"claude-sonnet-4.5\",\"name\":\"Claude Sonnet 4.5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-4.5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; + "{\"data\":[{\"id\":\"claude-sonnet-5\",\"name\":\"Claude Sonnet 5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; } ///

A single request the callback intercepted. diff --git a/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs b/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs index fd00cc9b99..2fca912592 100644 --- a/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs +++ b/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs @@ -76,15 +76,15 @@ public async Task Threads_The_Session_Id_Into_A_Byok_Session_Inference_Request() { OnPermissionRequest = PermissionHandler.ApproveAll, // BYOK providers require an explicit model id. - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = new ProviderConfig { Type = "openai", WireApi = "responses", BaseUrl = "https://byok.invalid/v1", ApiKey = "byok-secret", - ModelId = "claude-sonnet-4.5", - WireModel = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", + WireModel = "claude-sonnet-5", }, }); var byokSessionId = session.SessionId; diff --git a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs index 80ccdb8c90..e6890d6992 100644 --- a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs +++ b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs @@ -349,7 +349,7 @@ private static (string Type, string Json)[] ResponseEvents(string text, string i ]; private const string ModelCatalogJson = - "{\"data\":[{\"id\":\"claude-sonnet-4.5\",\"name\":\"Claude Sonnet 4.5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"supported_endpoints\":[\"/responses\",\"ws:/responses\"],\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-4.5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; + "{\"data\":[{\"id\":\"claude-sonnet-5\",\"name\":\"Claude Sonnet 5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"supported_endpoints\":[\"/responses\",\"ws:/responses\"],\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; private static int GetFreePort() { diff --git a/dotnet/test/E2E/RewindE2ETests.cs b/dotnet/test/E2E/RewindE2ETests.cs index 3152b2e908..e8379f988d 100644 --- a/dotnet/test/E2E/RewindE2ETests.cs +++ b/dotnet/test/E2E/RewindE2ETests.cs @@ -24,7 +24,7 @@ public async Task Should_Restore_Tracked_File_And_Conversation() await File.WriteAllTextAsync(filePath, OriginalFileContent); await using var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EnableFileChangeTracking = true, }); diff --git a/dotnet/test/E2E/RpcServerE2ETests.cs b/dotnet/test/E2E/RpcServerE2ETests.cs index 2df8593cc4..5e39c5ca5a 100644 --- a/dotnet/test/E2E/RpcServerE2ETests.cs +++ b/dotnet/test/E2E/RpcServerE2ETests.cs @@ -171,7 +171,7 @@ public async Task Should_Call_Rpc_Models_List_With_Typed_Result() var result = await client.Rpc.Models.ListAsync(); Assert.NotNull(result.Models); - Assert.Contains(result.Models, model => model.Id == "claude-sonnet-4.5"); + Assert.Contains(result.Models, model => model.Id == "claude-sonnet-5"); Assert.All(result.Models, model => Assert.False(string.IsNullOrWhiteSpace(model.Name))); } diff --git a/dotnet/test/E2E/RpcSessionStateE2ETests.cs b/dotnet/test/E2E/RpcSessionStateE2ETests.cs index 803c1c602b..196861cd9b 100644 --- a/dotnet/test/E2E/RpcSessionStateE2ETests.cs +++ b/dotnet/test/E2E/RpcSessionStateE2ETests.cs @@ -22,14 +22,14 @@ private static async Task AssertImplementedFailureAsync(Func ac [Fact] public async Task Should_Call_Session_Rpc_Model_GetCurrent() { - await using var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); + await using var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-5" }); var result = await session.Rpc.Model.GetCurrentAsync(); Assert.NotNull(result.ModelId); Assert.NotEmpty(result.ModelId); // Strengthen: verify the configured model is actually in effect, not just any model - Assert.Equal("claude-sonnet-4.5", result.ModelId); + Assert.Equal("claude-sonnet-5", result.ModelId); } [Fact] @@ -48,12 +48,12 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo() await using var session = await isolatedCtx.CreateSessionAsync(isolatedClient, new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", OnPermissionRequest = PermissionHandler.ApproveAll, }); var before = await session.Rpc.Model.GetCurrentAsync(); - Assert.Equal("claude-sonnet-4.5", before.ModelId); + Assert.Equal("claude-sonnet-5", before.ModelId); var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-5.4", reasoningEffort: "high"); Assert.Equal("gpt-5.4", result.ModelId); @@ -281,14 +281,14 @@ public async Task Should_Call_Metadata_Snapshot_SetWorkingDirectory_And_RecordCo var branch = $"rpc-context-{Guid.NewGuid():N}"; await using var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", WorkingDirectory = firstDirectory, }); var initialSnapshot = await session.Rpc.Metadata.SnapshotAsync(); Assert.Equal(session.SessionId, initialSnapshot.SessionId); Assert.Equal(MetadataSnapshotCurrentMode.Interactive, initialSnapshot.CurrentMode); - Assert.Equal("claude-sonnet-4.5", initialSnapshot.SelectedModel); + Assert.Equal("claude-sonnet-5", initialSnapshot.SelectedModel); Assert.False(initialSnapshot.IsRemote); Assert.False(initialSnapshot.AlreadyInUse); Assert.NotEqual(default, initialSnapshot.StartTime); @@ -405,14 +405,14 @@ public async Task Should_Set_ReasoningEffort_And_Auto_Name() { await using var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", }); var reasoning = await session.Rpc.Model.SetReasoningEffortAsync("high"); Assert.Equal("high", reasoning.ReasoningEffort); var currentModel = await session.Rpc.Model.GetCurrentAsync(); - Assert.Equal("claude-sonnet-4.5", currentModel.ModelId); + Assert.Equal("claude-sonnet-5", currentModel.ModelId); Assert.Equal("high", currentModel.ReasoningEffort); var autoName = $"Auto Session {Guid.NewGuid():N}"; @@ -653,9 +653,9 @@ public async Task Should_Compact_Session_History_After_Messages() var contextInfo = await session.Rpc.Metadata.ContextInfoAsync( promptTokenLimit: 128_000, outputTokenLimit: 4_096, - selectedModel: "claude-sonnet-4.5"); + selectedModel: "claude-sonnet-5"); var context = Assert.IsType(contextInfo.ContextInfo); - Assert.Equal("claude-sonnet-4.5", context.ModelName); + Assert.Equal("claude-sonnet-5", context.ModelName); Assert.Equal(128_000, context.PromptTokenLimit); Assert.True(context.Limit >= context.PromptTokenLimit); Assert.True(context.TotalTokens > 0); @@ -666,7 +666,7 @@ public async Task Should_Compact_Session_History_After_Messages() context.SystemTokens + context.ConversationTokens + context.ToolDefinitionsTokens, context.TotalTokens); - var recomputed = await session.Rpc.Metadata.RecomputeContextTokensAsync("claude-sonnet-4.5"); + var recomputed = await session.Rpc.Metadata.RecomputeContextTokensAsync("claude-sonnet-5"); Assert.True(recomputed.SystemTokenCount > 0); Assert.True(recomputed.MessagesTokenCount > 0); Assert.Equal(recomputed.SystemTokenCount + recomputed.MessagesTokenCount, recomputed.TotalTokens); diff --git a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs index 72663c35a4..234a5d8694 100644 --- a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs +++ b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs @@ -32,7 +32,7 @@ public async Task Should_List_Models_For_Session() await using var client = CreateAuthenticatedClient(token); await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -41,7 +41,7 @@ public async Task Should_List_Models_For_Session() Assert.NotNull(result.List); Assert.NotEmpty(result.List); // The configured model must be present in the returned catalog. - Assert.Contains(result.List, model => model.GetRawText().Contains("claude-sonnet-4.5", StringComparison.Ordinal)); + Assert.Contains(result.List, model => model.GetRawText().Contains("claude-sonnet-5", StringComparison.Ordinal)); } [Fact] @@ -73,7 +73,7 @@ public async Task Should_Add_Byok_Provider_And_Model_At_Runtime() Provider = providerName, Id = modelId, Name = "SDK Runtime Model", - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", WireModel = "wire-sdk-runtime-model", MaxContextWindowTokens = 4_096, MaxPromptTokens = 3_072, @@ -277,7 +277,7 @@ public async Task Should_Update_And_Clear_Live_Subagent_Settings() { ["general-purpose"] = new() { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EffortLevel = "high", ContextTier = SubagentSettingsEntryContextTier.Default, }, diff --git a/dotnet/test/E2E/SessionConfigE2ETests.cs b/dotnet/test/E2E/SessionConfigE2ETests.cs index 1bc4c52eb9..314ba2f0cf 100644 --- a/dotnet/test/E2E/SessionConfigE2ETests.cs +++ b/dotnet/test/E2E/SessionConfigE2ETests.cs @@ -31,7 +31,7 @@ public async Task Vision_Disabled_Then_Enabled_Via_SetModel() var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", ModelCapabilities = new ModelCapabilitiesOverride { Supports = new ModelCapabilitiesOverrideSupports { Vision = false }, @@ -46,7 +46,7 @@ public async Task Vision_Disabled_Then_Enabled_Via_SetModel() // Switch vision on await session.SetModelAsync( - "claude-sonnet-4.5", + "claude-sonnet-5", reasoningEffort: null, modelCapabilities: new ModelCapabilitiesOverride { @@ -74,7 +74,7 @@ public async Task Vision_Enabled_Then_Disabled_Via_SetModel() var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", ModelCapabilities = new ModelCapabilitiesOverride { Supports = new ModelCapabilitiesOverrideSupports { Vision = true }, @@ -89,7 +89,7 @@ public async Task Vision_Enabled_Then_Disabled_Via_SetModel() // Switch vision off await session.SetModelAsync( - "claude-sonnet-4.5", + "claude-sonnet-5", reasoningEffort: null, modelCapabilities: new ModelCapabilitiesOverride { @@ -216,7 +216,7 @@ public async Task Should_Forward_Custom_Provider_Headers_On_Create() { var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = CreateProxyProvider("create-provider-header"), }); @@ -240,7 +240,7 @@ public async Task Should_Forward_Custom_Provider_Headers_On_Resume() var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = CreateProxyProvider("resume-provider-header"), }); @@ -267,7 +267,7 @@ public async Task Should_Forward_Provider_Wire_Model() // tests for serialization coverage). var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = new ProviderConfig { Type = "openai", @@ -300,14 +300,14 @@ public async Task Should_Use_Provider_Model_Id_As_Wire_Model() Type = "openai", BaseUrl = Ctx.ProxyUrl, ApiKey = "test-provider-key", - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", }, }); await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); var exchange = Assert.Single(await Ctx.GetExchangesAsync()); - Assert.Equal("claude-sonnet-4.5", exchange.Request.Model); + Assert.Equal("claude-sonnet-5", exchange.Request.Model); await session.DisposeAsync(); } @@ -598,7 +598,7 @@ public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Crea var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EnableCitations = true, Provider = CreateAnthropicProvider(), }); @@ -645,7 +645,7 @@ public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Resu var session2 = await Ctx.ResumeSessionAsync(resumeClient, sessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EnableCitations = true, Provider = CreateAnthropicProvider(), }); @@ -834,8 +834,8 @@ private static ProviderConfig CreateAnthropicProvider() Type = "anthropic", BaseUrl = "https://anthropic-citations.invalid/v1", ApiKey = "test-provider-key", - ModelId = "claude-sonnet-4.5", - WireModel = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", + WireModel = "claude-sonnet-5", }; } diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs index 27ef7437f7..aababb670b 100644 --- a/dotnet/test/E2E/SessionE2ETests.cs +++ b/dotnet/test/E2E/SessionE2ETests.cs @@ -17,7 +17,7 @@ public class SessionE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : [Fact] public async Task ShouldCreateAndDisconnectSessions() { - var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); + var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-5" }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); diff --git a/dotnet/test/Harness/E2ETestBackend.cs b/dotnet/test/Harness/E2ETestBackend.cs index 04808ed7a9..71c8f85761 100644 --- a/dotnet/test/Harness/E2ETestBackend.cs +++ b/dotnet/test/Harness/E2ETestBackend.cs @@ -15,7 +15,7 @@ internal enum E2ETestBackend internal static class E2ETestBackendConfiguration { internal const string EnvironmentVariable = "COPILOT_SDK_E2E_BACKEND"; - private const string AnthropicDefaultModel = "claude-sonnet-4.5"; + private const string AnthropicDefaultModel = "claude-sonnet-5"; private const string OpenAIDefaultModel = "gpt-4.1"; private const string FakeCredential = "fake-byok-credential-for-e2e-tests"; diff --git a/dotnet/test/Unit/E2ETestBackendTests.cs b/dotnet/test/Unit/E2ETestBackendTests.cs index f7c39a5083..43b23c1a48 100644 --- a/dotnet/test/Unit/E2ETestBackendTests.cs +++ b/dotnet/test/Unit/E2ETestBackendTests.cs @@ -25,7 +25,7 @@ public void RejectsUnknownBackend() () => E2ETestBackendConfiguration.Parse("unknown")); [Theory] - [InlineData("anthropic-messages", "anthropic", null, "claude-sonnet-4.5")] + [InlineData("anthropic-messages", "anthropic", null, "claude-sonnet-5")] [InlineData("openai-responses", "openai", "responses", "gpt-4.1")] [InlineData("openai-completions", "openai", "completions", "gpt-4.1")] public void AppliesProvider( diff --git a/go/internal/e2e/client_options_e2e_test.go b/go/internal/e2e/client_options_e2e_test.go index 86332eb6f1..9c7b77c984 100644 --- a/go/internal/e2e/client_options_e2e_test.go +++ b/go/internal/e2e/client_options_e2e_test.go @@ -329,7 +329,7 @@ func TestClientOptionsE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ SessionID: sessionID, ClientName: "go-sdk-e2e-client", - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", ReasoningEffort: "low", ReasoningSummary: copilot.ReasoningSummaryNone, ContextTier: copilot.ContextTierLongContext, @@ -388,7 +388,7 @@ func TestClientOptionsE2E(t *testing.T) { expectedValues := map[string]any{ "sessionId": sessionID, "clientName": "go-sdk-e2e-client", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "reasoningEffort": "low", "reasoningSummary": "none", "contextTier": "long_context", diff --git a/go/internal/e2e/copilot_request_helpers_test.go b/go/internal/e2e/copilot_request_helpers_test.go index 81d14f4d94..cd1ac63cf7 100644 --- a/go/internal/e2e/copilot_request_helpers_test.go +++ b/go/internal/e2e/copilot_request_helpers_test.go @@ -47,8 +47,8 @@ func sseFrame(eventType string, data map[string]any) string { func modelCatalogJSON(supportedEndpoints []string) string { model := map[string]any{ - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -56,7 +56,7 @@ func modelCatalogJSON(supportedEndpoints []string) string { "model_picker_enabled": true, "capabilities": map[string]any{ "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": map[string]any{ "max_context_window_tokens": 200000, @@ -141,7 +141,7 @@ func buildAnthropicMessageSSEBody(text string) string { "type": "message_start", "message": map[string]any{ "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", "content": []any{}, + "model": "claude-sonnet-5", "content": []any{}, "stop_reason": nil, "stop_sequence": nil, "usage": map[string]any{"input_tokens": 5, "output_tokens": 1}, }, @@ -188,7 +188,7 @@ func buildInferenceResponse(url string, bodyText string) *http.Response { base := func() map[string]any { return map[string]any{ "id": "chatcmpl-stub-1", "object": "chat.completion.chunk", - "created": 1, "model": "claude-sonnet-4.5", + "created": 1, "model": "claude-sonnet-5", } } c1 := base() @@ -215,7 +215,7 @@ func buildInferenceResponse(url string, bodyText string) *http.Response { "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": []any{map[string]any{"type": "text", "text": syntheticResponseText}}, "stop_reason": "end_turn", "stop_sequence": nil, @@ -225,7 +225,7 @@ func buildInferenceResponse(url string, bodyText string) *http.Response { } raw, _ := json.Marshal(map[string]any{ - "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, "model": "claude-sonnet-4.5", + "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, "model": "claude-sonnet-5", "choices": []any{map[string]any{"index": 0, "message": map[string]any{"role": "assistant", "content": syntheticResponseText}, "finish_reason": "stop"}}, "usage": map[string]any{"prompt_tokens": 5, "completion_tokens": 7, "total_tokens": 12}, }) diff --git a/go/internal/e2e/copilot_request_session_id_e2e_test.go b/go/internal/e2e/copilot_request_session_id_e2e_test.go index f7673bd457..46a62db1ab 100644 --- a/go/internal/e2e/copilot_request_session_id_e2e_test.go +++ b/go/internal/e2e/copilot_request_session_id_e2e_test.go @@ -139,14 +139,14 @@ func TestCopilotRequestSessionID(t *testing.T) { before := len(transport.inferenceRecords()) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: &copilot.ProviderConfig{ Type: "openai", WireAPI: "responses", BaseURL: "https://byok.invalid/v1", APIKey: "byok-secret", - ModelID: "claude-sonnet-4.5", - WireModel: "claude-sonnet-4.5", + ModelID: "claude-sonnet-5", + WireModel: "claude-sonnet-5", }, }) if err != nil { diff --git a/go/internal/e2e/rewind_e2e_test.go b/go/internal/e2e/rewind_e2e_test.go index ab73feadda..701e610775 100644 --- a/go/internal/e2e/rewind_e2e_test.go +++ b/go/internal/e2e/rewind_e2e_test.go @@ -32,7 +32,7 @@ func TestRewindE2E(t *testing.T) { t.Fatalf("Failed to create original file: %v", err) } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", EnableFileChangeTracking: copilot.Bool(true), OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) diff --git a/go/internal/e2e/rpc_e2e_test.go b/go/internal/e2e/rpc_e2e_test.go index fcf843814e..4380415701 100644 --- a/go/internal/e2e/rpc_e2e_test.go +++ b/go/internal/e2e/rpc_e2e_test.go @@ -128,7 +128,7 @@ func TestSessionRPCE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -150,7 +150,7 @@ func TestSessionRPCE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -194,7 +194,7 @@ func TestSessionRPCE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) diff --git a/go/internal/e2e/rpc_server_e2e_test.go b/go/internal/e2e/rpc_server_e2e_test.go index ed38fe1b80..f1aa5a19c7 100644 --- a/go/internal/e2e/rpc_server_e2e_test.go +++ b/go/internal/e2e/rpc_server_e2e_test.go @@ -64,12 +64,12 @@ func TestRPCServerE2E(t *testing.T) { if strings.TrimSpace(model.Name) == "" { t.Errorf("Model %q has empty Name", model.ID) } - if model.ID == "claude-sonnet-4.5" { + if model.ID == "claude-sonnet-5" { hasClaude = true } } if !hasClaude { - t.Errorf("Expected models list to contain 'claude-sonnet-4.5'") + t.Errorf("Expected models list to contain 'claude-sonnet-5'") } }) diff --git a/go/internal/e2e/rpc_session_state_e2e_test.go b/go/internal/e2e/rpc_session_state_e2e_test.go index 88673a9ba9..f4870dbac6 100644 --- a/go/internal/e2e/rpc_session_state_e2e_test.go +++ b/go/internal/e2e/rpc_session_state_e2e_test.go @@ -26,7 +26,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Run("should call session rpc model getCurrent", func(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { @@ -37,8 +37,8 @@ func TestRPCSessionStateE2E(t *testing.T) { if err != nil { t.Fatalf("Model.GetCurrent failed: %v", err) } - if result.ModelID == nil || *result.ModelID != "claude-sonnet-4.5" { - t.Fatalf("Expected current model claude-sonnet-4.5, got %+v", result) + if result.ModelID == nil || *result.ModelID != "claude-sonnet-5" { + t.Fatalf("Expected current model claude-sonnet-5, got %+v", result) } }) @@ -58,7 +58,7 @@ func TestRPCSessionStateE2E(t *testing.T) { switchCtx.ConfigureForTest(t) session, err := switchClient.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { @@ -485,7 +485,7 @@ func TestRPCSessionStateE2E(t *testing.T) { branch := "rpc-context-" + randomHex(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", WorkingDirectory: firstDirectory, OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) @@ -498,7 +498,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Fatalf("Metadata.Snapshot failed: %v", err) } if initial.SessionID != session.SessionID || initial.CurrentMode != rpc.MetadataSnapshotCurrentModeInteractive || - initial.SelectedModel == nil || *initial.SelectedModel != "claude-sonnet-4.5" || + initial.SelectedModel == nil || *initial.SelectedModel != "claude-sonnet-5" || initial.IsRemote || initial.AlreadyInUse || initial.StartTime.IsZero() || initial.ModifiedTime.IsZero() || initial.Workspace == nil || initial.WorkspacePath == nil || *initial.WorkspacePath == "" { t.Fatalf("Unexpected initial metadata snapshot: %+v", initial) @@ -630,7 +630,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Run("should set reasoning effort and auto name", func(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { @@ -648,9 +648,9 @@ func TestRPCSessionStateE2E(t *testing.T) { if err != nil { t.Fatalf("Model.GetCurrent failed: %v", err) } - if current.ModelID == nil || *current.ModelID != "claude-sonnet-4.5" || + if current.ModelID == nil || *current.ModelID != "claude-sonnet-5" || current.ReasoningEffort == nil || *current.ReasoningEffort != "high" { - t.Fatalf("Expected current model claude-sonnet-4.5/high, got %+v", current) + t.Fatalf("Expected current model claude-sonnet-5/high, got %+v", current) } autoName := "Auto Session " + randomHex(t) @@ -760,7 +760,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Fatal("Expected fresh session to be idle") } - model := "claude-sonnet-4.5" + model := "claude-sonnet-5" contextInfo, err := session.RPC.Metadata.ContextInfo(t.Context(), &rpc.MetadataContextInfoRequest{ PromptTokenLimit: 128000, OutputTokenLimit: 4096, diff --git a/go/internal/e2e/rpc_session_state_extras_e2e_test.go b/go/internal/e2e/rpc_session_state_extras_e2e_test.go index 99dc0b7572..c74ed45cc8 100644 --- a/go/internal/e2e/rpc_session_state_extras_e2e_test.go +++ b/go/internal/e2e/rpc_session_state_extras_e2e_test.go @@ -22,7 +22,7 @@ func TestRpcSessionStateExtras(t *testing.T) { authClient := newAuthenticatedClient(ctx, token) defer authClient.ForceStop() - session := createPortedSession(t, authClient, &copilot.SessionConfig{Model: "claude-sonnet-4.5"}) + session := createPortedSession(t, authClient, &copilot.SessionConfig{Model: "claude-sonnet-5"}) defer session.Disconnect() result, err := session.RPC.Model.List(t.Context()) @@ -38,13 +38,13 @@ func TestRpcSessionStateExtras(t *testing.T) { found := false for _, model := range result.List { data, err := json.Marshal(model) - if err == nil && strings.Contains(string(data), "claude-sonnet-4.5") { + if err == nil && strings.Contains(string(data), "claude-sonnet-5") { found = true break } } if !found { - t.Fatalf("Expected model list to include claude-sonnet-4.5, got %+v", result.List) + t.Fatalf("Expected model list to include claude-sonnet-5, got %+v", result.List) } }) diff --git a/go/internal/e2e/session_config_e2e_test.go b/go/internal/e2e/session_config_e2e_test.go index 2ce48e3b33..f1c267e230 100644 --- a/go/internal/e2e/session_config_e2e_test.go +++ b/go/internal/e2e/session_config_e2e_test.go @@ -119,8 +119,8 @@ func createAnthropicProvider() *copilot.ProviderConfig { Type: "anthropic", BaseURL: "https://anthropic-citations.invalid/v1", APIKey: "test-provider-key", - ModelID: "claude-sonnet-4.5", - WireModel: "claude-sonnet-4.5", + ModelID: "claude-sonnet-5", + WireModel: "claude-sonnet-5", } } @@ -184,6 +184,7 @@ func TestSessionConfigE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Model: "claude-sonnet-5", ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(false), @@ -208,7 +209,7 @@ func TestSessionConfigE2E(t *testing.T) { } // Switch vision on - if err := session.SetModel(t.Context(), "claude-sonnet-4.5", &copilot.SetModelOptions{ + if err := session.SetModel(t.Context(), "claude-sonnet-5", &copilot.SetModelOptions{ ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(true), @@ -238,6 +239,7 @@ func TestSessionConfigE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Model: "claude-sonnet-5", ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(true), @@ -262,7 +264,7 @@ func TestSessionConfigE2E(t *testing.T) { } // Switch vision off - if err := session.SetModel(t.Context(), "claude-sonnet-4.5", &copilot.SetModelOptions{ + if err := session.SetModel(t.Context(), "claude-sonnet-5", &copilot.SetModelOptions{ ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(false), @@ -420,7 +422,7 @@ func TestSessionConfigNewOptionsCopilotRequestE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", EnableCitations: copilot.Bool(true), Provider: createAnthropicProvider(), }) @@ -481,7 +483,7 @@ func TestSessionConfigNewOptionsCopilotRequestE2E(t *testing.T) { session2, err := resumeClient.ResumeSessionWithOptions(t.Context(), session1.SessionID, &copilot.ResumeSessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", EnableCitations: copilot.Bool(true), Provider: createAnthropicProvider(), }) @@ -589,7 +591,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: createProxyProvider(ctx, providerHeaderName, "create-provider-header"), }) if err != nil { @@ -633,7 +635,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: createProxyProvider(ctx, providerHeaderName, "resume-provider-header"), }) if err != nil { @@ -677,7 +679,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { maxOutputTokens := 1024 session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: &copilot.ProviderConfig{ Type: "openai", BaseURL: ctx.ProxyURL, @@ -719,7 +721,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { Type: "openai", BaseURL: ctx.ProxyURL, APIKey: "test-provider-key", - ModelID: "claude-sonnet-4.5", + ModelID: "claude-sonnet-5", }, }) if err != nil { @@ -738,8 +740,8 @@ func TestSessionConfigExtrasE2E(t *testing.T) { if len(exchanges) != 1 { t.Fatalf("Expected exactly 1 exchange, got %d", len(exchanges)) } - if exchanges[0].Request.Model != "claude-sonnet-4.5" { - t.Errorf("Expected request model to be 'claude-sonnet-4.5', got %q", exchanges[0].Request.Model) + if exchanges[0].Request.Model != "claude-sonnet-5" { + t.Errorf("Expected request model to be 'claude-sonnet-5', got %q", exchanges[0].Request.Model) } }) diff --git a/go/internal/e2e/session_e2e_test.go b/go/internal/e2e/session_e2e_test.go index cf39b6784c..efc20250b1 100644 --- a/go/internal/e2e/session_e2e_test.go +++ b/go/internal/e2e/session_e2e_test.go @@ -23,7 +23,7 @@ func TestSessionE2E(t *testing.T) { t.Run("should create and disconnect sessions", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Model: "claude-sonnet-4.5"}) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Model: "claude-sonnet-5"}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -47,8 +47,8 @@ func TestSessionE2E(t *testing.T) { t.Errorf("Expected session.start sessionId to match") } - if !startOk || startData.SelectedModel == nil || *startData.SelectedModel != "claude-sonnet-4.5" { - t.Errorf("Expected selectedModel to be 'claude-sonnet-4.5', got %v", startData) + if !startOk || startData.SelectedModel == nil || *startData.SelectedModel != "claude-sonnet-5" { + t.Errorf("Expected selectedModel to be 'claude-sonnet-5', got %v", startData) } if err := session.Disconnect(); err != nil { diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java b/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java index 3025c64c39..b4159d839c 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java @@ -77,11 +77,11 @@ void threadsSessionIdForCapiAndByok() throws Exception { // BYOK session. int before = handler.inferenceRequests().size(); ProviderConfig provider = new ProviderConfig().setType("openai").setWireApi("responses") - .setBaseUrl("https://byok.invalid/v1").setApiKey("byok-secret").setModelId("claude-sonnet-4.5") - .setWireModel("claude-sonnet-4.5"); + .setBaseUrl("https://byok.invalid/v1").setApiKey("byok-secret").setModelId("claude-sonnet-5") + .setWireModel("claude-sonnet-5"); CopilotSession byokSession = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setModel("claude-sonnet-4.5").setProvider(provider)) + .setModel("claude-sonnet-5").setProvider(provider)) .get(); String byokSessionId = byokSession.getSessionId(); diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java b/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java index aa173ef30e..fa2a6354be 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java @@ -134,7 +134,7 @@ static String anthropicMessageSseBody(String text) { startMessage.put("id", "msg_stub_1"); startMessage.put("type", "message"); startMessage.put("role", "assistant"); - startMessage.put("model", "claude-sonnet-4.5"); + startMessage.put("model", "claude-sonnet-5"); startMessage.put("content", List.of()); startMessage.put("stop_reason", null); startMessage.put("stop_sequence", null); @@ -251,7 +251,7 @@ static HttpResponse buildInferenceResponse(String url, String bodyT body.put("id", "msg_stub_1"); body.put("type", "message"); body.put("role", "assistant"); - body.put("model", "claude-sonnet-4.5"); + body.put("model", "claude-sonnet-5"); body.put("content", List.of(Map.of("type", "text", "text", text))); body.put("stop_reason", "end_turn"); body.put("stop_sequence", null); @@ -301,14 +301,14 @@ static String modelCatalog(List supportedEndpoints) { Map capabilities = new LinkedHashMap<>(); capabilities.put("type", "chat"); - capabilities.put("family", "claude-sonnet-4.5"); + capabilities.put("family", "claude-sonnet-5"); capabilities.put("tokenizer", "o200k_base"); capabilities.put("limits", limits); capabilities.put("supports", supports); Map model = new LinkedHashMap<>(); - model.put("id", "claude-sonnet-4.5"); - model.put("name", "Claude Sonnet 4.5"); + model.put("id", "claude-sonnet-5"); + model.put("name", "Claude Sonnet 5"); model.put("object", "model"); model.put("vendor", "Anthropic"); model.put("version", "1"); @@ -416,7 +416,7 @@ private static Map chatChunkBase() { base.put("id", "chatcmpl-stub-1"); base.put("object", "chat.completion.chunk"); base.put("created", 1); - base.put("model", "claude-sonnet-4.5"); + base.put("model", "claude-sonnet-5"); return base; } @@ -459,7 +459,7 @@ private static Map chatCompletion(String text) { root.put("id", "chatcmpl-stub-1"); root.put("object", "chat.completion"); root.put("created", 1); - root.put("model", "claude-sonnet-4.5"); + root.put("model", "claude-sonnet-5"); root.put("choices", List.of(choice)); root.put("usage", chatUsage()); return root; diff --git a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java index 2393f334b2..6c9753025a 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java @@ -135,7 +135,7 @@ void testShouldCallRpcModelsListWithTypedResult() throws Exception { var result = client.getRpc().models.list().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); assertNotNull(result.models()); - assertTrue(result.models().stream().anyMatch(model -> "claude-sonnet-4.5".equals(model.id()))); + assertTrue(result.models().stream().anyMatch(model -> "claude-sonnet-5".equals(model.id()))); result.models().forEach(model -> { assertFalse(model.id().isBlank()); assertFalse(model.name().isBlank()); diff --git a/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java b/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java index 925fd6d873..e786bda994 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java @@ -125,7 +125,7 @@ void testShouldForwardProviderWireModel() throws Exception { try (CopilotClient client = ctx.createClient()) { CopilotSession session = client - .createSession(new SessionConfig().setModel("claude-sonnet-4.5") + .createSession(new SessionConfig().setModel("claude-sonnet-5") .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) .setApiKey("test-provider-key").setWireModel("test-wire-model") .setMaxOutputTokens(1024)) @@ -149,7 +149,7 @@ void testShouldUseProviderModelIdAsWireModel() throws Exception { try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(new SessionConfig() .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) - .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5")) + .setApiKey("test-provider-key").setModelId("claude-sonnet-5")) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS); @@ -158,7 +158,7 @@ void testShouldUseProviderModelIdAsWireModel() throws Exception { assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); @SuppressWarnings("unchecked") Map request = (Map) exchanges.get(0).get("request"); - assertEquals("claude-sonnet-4.5", request.get("model")); + assertEquals("claude-sonnet-5", request.get("model")); } } @@ -272,7 +272,7 @@ void testShouldEnableCitationsForAnthropicFileAttachmentsOnCreate() throws Excep var handler = new CopilotRequestTestSupport.RecordingRequestHandler(SYNTHETIC_TEXT); try (CopilotClient client = newLlmClient(ctx, handler)) { - CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-4.5") + CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-5") .setEnableCitations(true).setProvider(createAnthropicProvider()) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); @@ -296,7 +296,7 @@ void testShouldEnableCitationsForAnthropicFileAttachmentsOnResume() throws Excep CopilotSession session1 = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); CopilotSession session2 = client.resumeSession(session1.getSessionId(), - new ResumeSessionConfig().setModel("claude-sonnet-4.5").setEnableCitations(true) + new ResumeSessionConfig().setModel("claude-sonnet-5").setEnableCitations(true) .setProvider(createAnthropicProvider()) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) .get(); @@ -388,7 +388,7 @@ private static BlobAttachment createPdfAttachment() { private static ProviderConfig createAnthropicProvider() { return new ProviderConfig().setType("anthropic").setBaseUrl("https://anthropic-citations.invalid/v1") - .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5").setWireModel("claude-sonnet-4.5"); + .setApiKey("test-provider-key").setModelId("claude-sonnet-5").setWireModel("claude-sonnet-5"); } private static String singleInferenceRequestBody(CopilotRequestTestSupport.RecordingRequestHandler handler) { diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java index 64eff21726..fd3f92100d 100644 --- a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java +++ b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java @@ -60,10 +60,8 @@ void shouldRestoreTrackedFileAndConversation() throws Exception { Files.writeString(filePath, ORIGINAL_FILE_CONTENT); try (CopilotClient client = ctx.createClient(); - CopilotSession session = client - .createSession( - new SessionConfig().setModel("claude-sonnet-4.5").setEnableFileChangeTracking(true) - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-5") + .setEnableFileChangeTracking(true).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) .get(30, TimeUnit.SECONDS)) { AssistantMessageEvent ready = session .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of " diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 638f535582..eb0bc3d723 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -326,10 +326,10 @@ void sessionModelGetCurrentParams_record() { @Test void sessionModelSwitchToParams_record() { - var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-4.5", "high", null, null, null, null, - null, null, null, null, null, null, null, null); + var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-5", "high", null, null, null, null, null, + null, null, null, null, null, null, null); assertEquals("sess-32", params.sessionId()); - assertEquals("claude-sonnet-4.5", params.modelId()); + assertEquals("claude-sonnet-5", params.modelId()); assertEquals("high", params.reasoningEffort()); assertNull(params.reasoningSummary()); assertNull(params.verbosity()); @@ -656,8 +656,8 @@ void sessionMcpListResult_status_enum_all_values() { @Test void sessionModelGetCurrentResult_record() { - var result = new SessionModelGetCurrentResult("claude-sonnet-4.5", null, null); - assertEquals("claude-sonnet-4.5", result.modelId()); + var result = new SessionModelGetCurrentResult("claude-sonnet-5", null, null); + assertEquals("claude-sonnet-5", result.modelId()); } @Test diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index e3dc41343b..2f1dee6cb5 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -467,7 +467,7 @@ describe("Client options", async () => { }); const session = await client.createSession({ clientName: "advanced-create-client", - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", reasoningEffort: "medium", reasoningSummary: "detailed", contextTier: "long_context", @@ -532,7 +532,7 @@ describe("Client options", async () => { provider: "create-provider", id: "create-model", name: "Create Model", - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", wireModel: "create-wire-model", maxContextWindowTokens: 12_000, maxPromptTokens: 10_000, @@ -544,7 +544,7 @@ describe("Client options", async () => { const createRequest = getCapturedRequest(capturePath, "session.create"); expect(createRequest.clientName).toBe("advanced-create-client"); - expect(createRequest.model).toBe("claude-sonnet-4.5"); + expect(createRequest.model).toBe("claude-sonnet-5"); expect(createRequest.reasoningEffort).toBe("medium"); expect(createRequest.reasoningSummary).toBe("detailed"); expect(createRequest.contextTier).toBe("long_context"); @@ -609,7 +609,7 @@ describe("Client options", async () => { await client.start(); const session = await client.createSession({ - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: { type: "azure", wireApi: "responses", @@ -619,7 +619,7 @@ describe("Client options", async () => { bearerToken: "provider-bearer-token", azure: { apiVersion: "2024-02-15-preview" }, headers: { "X-Provider-Wire": "yes" }, - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", wireModel: "azure-deployment", maxPromptTokens: 8192, maxOutputTokens: 1024, @@ -636,7 +636,7 @@ describe("Client options", async () => { expect(provider.bearerToken).toBe("provider-bearer-token"); expect(getObject(provider.azure).apiVersion).toBe("2024-02-15-preview"); expect(getObject(provider.headers)["X-Provider-Wire"]).toBe("yes"); - expect(provider.modelId).toBe("claude-sonnet-4.5"); + expect(provider.modelId).toBe("claude-sonnet-5"); expect(provider.wireModel).toBe("azure-deployment"); expect(provider.maxPromptTokens).toBe(8192); expect(provider.maxOutputTokens).toBe(1024); diff --git a/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts b/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts index 69bacd4f6e..b0af6524a1 100644 --- a/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts @@ -56,8 +56,8 @@ function serveNonInference(url: string): Response { const MODEL_CATALOG_JSON = JSON.stringify({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -65,7 +65,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({ model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { diff --git a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts index 309250d852..04bccb7e08 100644 --- a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts @@ -42,8 +42,8 @@ async function startFakeUpstream(): Promise<{ sendJson(res, 200, { data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -52,7 +52,7 @@ async function startFakeUpstream(): Promise<{ supported_endpoints: ["/responses", "ws:/responses"], capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, diff --git a/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts b/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts index bd070c20ca..9b0dbbb3bc 100644 --- a/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts @@ -171,7 +171,7 @@ const CHAT_COMPLETION_STREAM_EVENTS: string[] = (() => { id: "chatcmpl-stub-1", object: "chat.completion.chunk", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }; return [ `data: ${JSON.stringify({ @@ -210,7 +210,7 @@ const BUFFERED_CHAT_COMPLETION_JSON = JSON.stringify({ id: "chatcmpl-stub-1", object: "chat.completion", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -224,8 +224,8 @@ const BUFFERED_CHAT_COMPLETION_JSON = JSON.stringify({ const MODEL_CATALOG_JSON = JSON.stringify({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -233,7 +233,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({ model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { @@ -300,14 +300,14 @@ describe("CopilotRequestHandler threads the runtime session id (CAPI + BYOK)", a const session = await client.createSession({ onPermissionRequest: approveAll, // BYOK providers require an explicit model id. - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: { type: "openai", wireApi: "responses", baseUrl: "https://byok.invalid/v1", apiKey: "byok-secret", - modelId: "claude-sonnet-4.5", - wireModel: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", + wireModel: "claude-sonnet-5", }, }); const byokSessionId = session.sessionId; diff --git a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts index 611898f11d..8478f93288 100644 --- a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts +++ b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts @@ -154,7 +154,7 @@ const CHAT_COMPLETION_STREAM = [ id: "persisted-session", object: "chat.completion.chunk", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -167,7 +167,7 @@ const CHAT_COMPLETION_STREAM = [ id: "persisted-session", object: "chat.completion.chunk", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [{ index: 0, delta: {}, finish_reason: "stop" }], }, ] @@ -179,7 +179,7 @@ const CHAT_COMPLETION_RESPONSE_JSON = JSON.stringify({ id: "persisted-session", object: "chat.completion", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -193,8 +193,8 @@ const CHAT_COMPLETION_RESPONSE_JSON = JSON.stringify({ const MODEL_CATALOG_JSON = JSON.stringify({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -202,7 +202,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({ model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { streaming: true, tool_calls: true, parallel_tool_calls: true }, diff --git a/nodejs/test/e2e/rewind.e2e.test.ts b/nodejs/test/e2e/rewind.e2e.test.ts index c64fe83145..7fdfee94c8 100644 --- a/nodejs/test/e2e/rewind.e2e.test.ts +++ b/nodejs/test/e2e/rewind.e2e.test.ts @@ -30,7 +30,7 @@ describe("Rewind", async () => { const filePath = join(workDir, FILE_NAME); writeFileSync(filePath, ORIGINAL_FILE_CONTENT); const session = await client.createSession({ - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", enableFileChangeTracking: true, onPermissionRequest: approveAll, }); diff --git a/nodejs/test/e2e/rpc.e2e.test.ts b/nodejs/test/e2e/rpc.e2e.test.ts index f90547da9b..4e93fdbe44 100644 --- a/nodejs/test/e2e/rpc.e2e.test.ts +++ b/nodejs/test/e2e/rpc.e2e.test.ts @@ -73,7 +73,7 @@ describe("Session RPC", async () => { it.skip("should call session.rpc.model.getCurrent", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); const result = await session.rpc.model.getCurrent(); @@ -85,7 +85,7 @@ describe("Session RPC", async () => { it.skip("should call session.rpc.model.switchTo", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); // Get initial model diff --git a/nodejs/test/e2e/rpc_server.e2e.test.ts b/nodejs/test/e2e/rpc_server.e2e.test.ts index 5075ae68d9..13a63875e9 100644 --- a/nodejs/test/e2e/rpc_server.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server.e2e.test.ts @@ -144,7 +144,7 @@ describe("Server-scoped RPC", async () => { const result = await authClient.listModels(); expect(Array.isArray(result)).toBe(true); - expect(result.some((m) => m.id === "claude-sonnet-4.5")).toBe(true); + expect(result.some((m) => m.id === "claude-sonnet-5")).toBe(true); for (const model of result) { expect(model.name).toBeTruthy(); } diff --git a/nodejs/test/e2e/rpc_session_state.e2e.test.ts b/nodejs/test/e2e/rpc_session_state.e2e.test.ts index 5164f99232..aab08b3bc5 100644 --- a/nodejs/test/e2e/rpc_session_state.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state.e2e.test.ts @@ -40,7 +40,7 @@ describe("Session-scoped RPC", async () => { it("should call session rpc model getcurrent", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); const result = await session.rpc.model.getCurrent(); @@ -65,7 +65,7 @@ describe("Session-scoped RPC", async () => { it("should call session rpc model switchto", async () => { const session = await switchClient.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); const before = await session.rpc.model.getCurrent(); @@ -315,14 +315,14 @@ describe("Session-scoped RPC", async () => { const branch = `rpc-context-${randomUUID()}`; const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", workingDirectory: firstDirectory, }); try { const initialSnapshot = await session.rpc.metadata.snapshot(); expect(initialSnapshot.sessionId).toBe(session.sessionId); expect(initialSnapshot.currentMode).toBe("interactive"); - expect(initialSnapshot.selectedModel).toBe("claude-sonnet-4.5"); + expect(initialSnapshot.selectedModel).toBe("claude-sonnet-5"); expect(initialSnapshot.isRemote).toBe(false); expect(initialSnapshot.alreadyInUse).toBe(false); expect(Date.parse(initialSnapshot.startTime)).not.toBeNaN(); @@ -446,7 +446,7 @@ describe("Session-scoped RPC", async () => { it("should set reasoning effort and auto name", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); try { const reasoning = await session.rpc.model.setReasoningEffort({ @@ -455,7 +455,7 @@ describe("Session-scoped RPC", async () => { expect(reasoning.reasoningEffort).toBe("high"); const currentModel = await session.rpc.model.getCurrent(); - expect(currentModel.modelId).toBe("claude-sonnet-4.5"); + expect(currentModel.modelId).toBe("claude-sonnet-5"); expect(currentModel.reasoningEffort).toBe("high"); const autoName = `Auto Session ${randomUUID()}`; @@ -734,11 +734,11 @@ describe("Session-scoped RPC", async () => { const contextInfo = await session.rpc.metadata.contextInfo({ promptTokenLimit: 128_000, outputTokenLimit: 4_096, - selectedModel: "claude-sonnet-4.5", + selectedModel: "claude-sonnet-5", }); expect(contextInfo.contextInfo).not.toBeNull(); if (contextInfo.contextInfo) { - expect(contextInfo.contextInfo.modelName).toBe("claude-sonnet-4.5"); + expect(contextInfo.contextInfo.modelName).toBe("claude-sonnet-5"); expect(contextInfo.contextInfo.promptTokenLimit).toBe(128_000); expect(contextInfo.contextInfo.limit).toBeGreaterThanOrEqual( contextInfo.contextInfo.promptTokenLimit @@ -755,7 +755,7 @@ describe("Session-scoped RPC", async () => { } const recomputed = await session.rpc.metadata.recomputeContextTokens({ - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", }); expect(recomputed.systemTokenCount).toBeGreaterThan(0); expect(recomputed.messagesTokenCount).toBeGreaterThan(0); diff --git a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts index 6111809914..7b88af7e2d 100644 --- a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts @@ -70,7 +70,7 @@ describe("Session-scoped state extras RPC", async () => { try { await authClient.start(); session = await authClient.createSession({ - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", onPermissionRequest: approveAll, }); @@ -79,7 +79,7 @@ describe("Session-scoped state extras RPC", async () => { expect(Array.isArray(result.list)).toBe(true); expect(result.list.length).toBeGreaterThan(0); expect( - result.list.some((model) => JSON.stringify(model).includes("claude-sonnet-4.5")) + result.list.some((model) => JSON.stringify(model).includes("claude-sonnet-5")) ).toBe(true); } finally { await disconnect(session); @@ -126,7 +126,7 @@ describe("Session-scoped state extras RPC", async () => { provider: providerName, id: modelId, name: "SDK Runtime Model", - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", wireModel: "wire-sdk-runtime-model", maxContextWindowTokens: 4096, maxPromptTokens: 3072, diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index b89221998a..bab0efc687 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -98,7 +98,7 @@ describe("Sessions", () => { it("should create and disconnect sessions", async () => { await using session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); @@ -107,7 +107,7 @@ describe("Sessions", () => { expect(sessionStartEvents).toMatchObject([ { type: "session.start", - data: { sessionId: session.sessionId, selectedModel: "claude-sonnet-4.5" }, + data: { sessionId: session.sessionId, selectedModel: "claude-sonnet-5" }, }, ]); diff --git a/nodejs/test/e2e/session_config.e2e.test.ts b/nodejs/test/e2e/session_config.e2e.test.ts index 85137e0ff9..8d041f1ec8 100644 --- a/nodejs/test/e2e/session_config.e2e.test.ts +++ b/nodejs/test/e2e/session_config.e2e.test.ts @@ -119,6 +119,7 @@ describe("Session Configuration", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, + model: "claude-sonnet-5", modelCapabilities: { supports: { vision: false } }, }); @@ -129,7 +130,7 @@ describe("Session Configuration", async () => { expect(hasImageUrlContent(t1Messages)).toBe(false); // Switch vision on (re-specify same model with updated capabilities) - await session.setModel("claude-sonnet-4.5", { + await session.setModel("claude-sonnet-5", { modelCapabilities: { supports: { vision: true } }, }); @@ -149,6 +150,7 @@ describe("Session Configuration", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, + model: "claude-sonnet-5", modelCapabilities: { supports: { vision: true } }, }); @@ -159,7 +161,7 @@ describe("Session Configuration", async () => { expect(hasImageUrlContent(t1Messages)).toBe(true); // Switch vision off - await session.setModel("claude-sonnet-4.5", { + await session.setModel("claude-sonnet-5", { modelCapabilities: { supports: { vision: false } }, }); @@ -342,7 +344,7 @@ describe("Session Configuration", async () => { id: "msg_stub_1", type: "message", role: "assistant", - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", content: [], stop_reason: null, stop_sequence: null, @@ -384,8 +386,8 @@ describe("Session Configuration", async () => { return json({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -393,7 +395,7 @@ describe("Session Configuration", async () => { model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { @@ -423,7 +425,7 @@ describe("Session Configuration", async () => { id: "msg_stub_1", type: "message", role: "assistant", - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", content: [{ type: "text", text: "OK from the synthetic stream." }], stop_reason: "end_turn", stop_sequence: null, @@ -434,7 +436,7 @@ describe("Session Configuration", async () => { id: "chatcmpl-stub-1", object: "chat.completion", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -462,8 +464,8 @@ describe("Session Configuration", async () => { type: "anthropic" as const, baseUrl: "https://anthropic-citations.invalid/v1", apiKey: "test-provider-key", - modelId: "claude-sonnet-4.5", - wireModel: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", + wireModel: "claude-sonnet-5", }; } @@ -563,7 +565,7 @@ describe("Session Configuration", async () => { try { const session = await citationClient.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", enableCitations: true, provider: createAnthropicProvider(), }); @@ -607,7 +609,7 @@ describe("Session Configuration", async () => { try { const session2 = await resumeClient.resumeSession(session1.sessionId, { onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", enableCitations: true, provider: createAnthropicProvider(), }); @@ -711,7 +713,7 @@ describe("Session Configuration", async () => { it("should forward custom provider headers on create", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: createProxyProvider("create-provider-header"), }); @@ -734,7 +736,7 @@ describe("Session Configuration", async () => { const session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: createProxyProvider("resume-provider-header"), }); @@ -762,7 +764,7 @@ describe("Session Configuration", async () => { // tests for serialization coverage). const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: { type: "openai", baseUrl: openAiEndpoint.url, @@ -791,7 +793,7 @@ describe("Session Configuration", async () => { type: "openai", baseUrl: openAiEndpoint.url, apiKey: "test-provider-key", - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", }, }); @@ -799,7 +801,7 @@ describe("Session Configuration", async () => { const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges.length).toBe(1); - expect(exchanges[0].request.model).toBe("claude-sonnet-4.5"); + expect(exchanges[0].request.model).toBe("claude-sonnet-5"); await session.disconnect(); }); diff --git a/python/e2e/_copilot_request_helpers.py b/python/e2e/_copilot_request_helpers.py index 2d91bc9bc2..d4073dd197 100644 --- a/python/e2e/_copilot_request_helpers.py +++ b/python/e2e/_copilot_request_helpers.py @@ -58,8 +58,8 @@ def _wants_stream(body: bytes) -> bool: def model_catalog(supported_endpoints: list[str] | None = None) -> dict: """The synthetic ``/models`` catalog payload.""" model: dict = { - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -67,7 +67,7 @@ def model_catalog(supported_endpoints: list[str] | None = None) -> dict: "model_picker_enabled": True, "capabilities": { "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": {"max_context_window_tokens": 200000, "max_output_tokens": 8192}, "supports": { @@ -191,7 +191,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "chatcmpl-stub-1", "object": "chat.completion.chunk", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", } chunks = [ { @@ -234,7 +234,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [], "stop_reason": None, "stop_sequence": None, @@ -283,7 +283,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [{"type": "text", "text": text}], "stop_reason": "end_turn", "stop_sequence": None, @@ -300,7 +300,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "choices": [ { "index": 0, diff --git a/python/e2e/test_client_options_e2e.py b/python/e2e/test_client_options_e2e.py index fe1ed54820..4bceabcb5e 100644 --- a/python/e2e/test_client_options_e2e.py +++ b/python/e2e/test_client_options_e2e.py @@ -396,7 +396,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request( await client.start() session = await client.create_session( client_name="advanced-create-client", - model="claude-sonnet-4.5", + model="claude-sonnet-5", reasoning_effort="medium", reasoning_summary="detailed", context_tier="long_context", @@ -470,7 +470,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request( "provider": "create-provider", "id": "create-model", "name": "Create Model", - "model_id": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", "wire_model": "create-wire-model", "max_context_window_tokens": 12_000, "max_prompt_tokens": 10_000, @@ -482,7 +482,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request( try: params = _get_captured_request(capture_path, "session.create") assert params["clientName"] == "advanced-create-client" - assert params["model"] == "claude-sonnet-4.5" + assert params["model"] == "claude-sonnet-5" assert params["reasoningEffort"] == "medium" assert params["reasoningSummary"] == "detailed" assert params["contextTier"] == "long_context" @@ -538,7 +538,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request( try: await client.start() session = await client.create_session( - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider={ "type": "azure", "wire_api": "responses", @@ -548,7 +548,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request( "bearer_token": "provider-bearer-token", "azure": {"api_version": "2024-02-15-preview"}, "headers": {"X-Provider-Wire": "yes"}, - "model_id": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", "wire_model": "azure-deployment", "max_prompt_tokens": 8192, "max_output_tokens": 1024, @@ -565,7 +565,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request( assert provider["bearerToken"] == "provider-bearer-token" assert provider["azure"]["apiVersion"] == "2024-02-15-preview" assert provider["headers"]["X-Provider-Wire"] == "yes" - assert provider["modelId"] == "claude-sonnet-4.5" + assert provider["modelId"] == "claude-sonnet-5" assert provider["wireModel"] == "azure-deployment" assert provider["maxPromptTokens"] == 8192 assert provider["maxOutputTokens"] == 1024 diff --git a/python/e2e/test_copilot_request_session_id_e2e.py b/python/e2e/test_copilot_request_session_id_e2e.py index 81624d73d0..75bf15f2a4 100644 --- a/python/e2e/test_copilot_request_session_id_e2e.py +++ b/python/e2e/test_copilot_request_session_id_e2e.py @@ -105,14 +105,14 @@ async def test_threads_session_id_into_byok_session(self, session_id_client): baseline = len(handler.records) session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider={ "type": "openai", "wire_api": "responses", "base_url": "https://byok.invalid/v1", "api_key": "byok-secret", - "model_id": "claude-sonnet-4.5", - "wire_model": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", + "wire_model": "claude-sonnet-5", }, ) byok_session_id = session.session_id diff --git a/python/e2e/test_rewind_e2e.py b/python/e2e/test_rewind_e2e.py index fa87c08b64..cf7627c1a3 100644 --- a/python/e2e/test_rewind_e2e.py +++ b/python/e2e/test_rewind_e2e.py @@ -35,7 +35,7 @@ async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestCo file_path = Path(ctx.work_dir) / FILE_NAME file_path.write_text(ORIGINAL_FILE_CONTENT, encoding="utf-8") session = await ctx.client.create_session( - model="claude-sonnet-4.5", + model="claude-sonnet-5", enable_file_change_tracking=True, on_permission_request=PermissionHandler.approve_all, ) diff --git a/python/e2e/test_rpc_e2e.py b/python/e2e/test_rpc_e2e.py index 4440635727..c9f08742cd 100644 --- a/python/e2e/test_rpc_e2e.py +++ b/python/e2e/test_rpc_e2e.py @@ -82,7 +82,7 @@ class TestSessionRpc: async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): """Test calling session.rpc.model.getCurrent""" session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" ) result = await session.rpc.model.get_current() @@ -96,7 +96,7 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext from copilot.rpc import ModelSwitchToRequest session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" ) # Get initial model diff --git a/python/e2e/test_rpc_server_e2e.py b/python/e2e/test_rpc_server_e2e.py index e7c4a446ce..fdff3b8004 100644 --- a/python/e2e/test_rpc_server_e2e.py +++ b/python/e2e/test_rpc_server_e2e.py @@ -183,7 +183,7 @@ async def test_should_call_rpc_models_list_with_typed_result(self, authed_ctx: E await client.start() result = await client.rpc.models.list(ModelsListRequest()) assert result.models is not None - assert any(model.id == "claude-sonnet-4.5" for model in result.models) + assert any(model.id == "claude-sonnet-5" for model in result.models) assert all((model.name or "").strip() for model in result.models) finally: try: diff --git a/python/e2e/test_rpc_session_state_e2e.py b/python/e2e/test_rpc_session_state_e2e.py index f4b03d2e65..622192cfe9 100644 --- a/python/e2e/test_rpc_session_state_e2e.py +++ b/python/e2e/test_rpc_session_state_e2e.py @@ -104,7 +104,7 @@ class TestRpcSessionState: async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", ) try: result = await session.rpc.model.get_current() @@ -128,7 +128,7 @@ async def test_should_call_session_rpc_model_switchto(self, ctx: E2ETestContext) ) session = await isolated_ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", ) try: before = await session.rpc.model.get_current() @@ -264,13 +264,13 @@ async def test_should_call_metadata_snapshot_set_working_directory_and_record_co session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", working_directory=first_dir, ) try: snapshot = await session.rpc.metadata.snapshot() assert snapshot.session_id == session.session_id - assert snapshot.selected_model == "claude-sonnet-4.5" + assert snapshot.selected_model == "claude-sonnet-5" assert snapshot.is_remote is False assert snapshot.already_in_use is False assert _path_equals(first_dir, snapshot.working_directory) @@ -395,7 +395,7 @@ async def snapshot_updated() -> bool: async def test_should_set_reasoning_effort_and_auto_name(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", ) try: reasoning = await session.rpc.model.set_reasoning_effort( @@ -403,7 +403,7 @@ async def test_should_set_reasoning_effort_and_auto_name(self, ctx: E2ETestConte ) assert reasoning.reasoning_effort == "high" current = await session.rpc.model.get_current() - assert current.model_id == "claude-sonnet-4.5" + assert current.model_id == "claude-sonnet-5" assert current.reasoning_effort == "high" auto_name = f"Auto Session {uuid.uuid4().hex}" @@ -637,12 +637,12 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC MetadataContextInfoRequest( prompt_token_limit=128_000, output_token_limit=4_096, - selected_model="claude-sonnet-4.5", + selected_model="claude-sonnet-5", ) ) if context_info.context_info is not None: context = context_info.context_info - assert context.model_name == "claude-sonnet-4.5" + assert context.model_name == "claude-sonnet-5" assert context.prompt_token_limit == 128_000 assert context.limit >= context.prompt_token_limit assert context.total_tokens > 0 @@ -657,7 +657,7 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC ) recomputed = await session.rpc.metadata.recompute_context_tokens( - MetadataRecomputeContextTokensRequest(model_id="claude-sonnet-4.5") + MetadataRecomputeContextTokensRequest(model_id="claude-sonnet-5") ) assert recomputed.system_token_count > 0 assert recomputed.messages_token_count > 0 diff --git a/python/e2e/test_rpc_session_state_extras_e2e.py b/python/e2e/test_rpc_session_state_extras_e2e.py index 7523059c7b..02ee0cd790 100644 --- a/python/e2e/test_rpc_session_state_extras_e2e.py +++ b/python/e2e/test_rpc_session_state_extras_e2e.py @@ -77,7 +77,7 @@ async def test_should_list_models_for_session(self, ctx: E2ETestContext): client = _make_authed_client(ctx, token) try: async with await client.create_session( - model="claude-sonnet-4.5", + model="claude-sonnet-5", on_permission_request=PermissionHandler.approve_all, github_token=token, ) as session: @@ -86,8 +86,7 @@ async def test_should_list_models_for_session(self, ctx: E2ETestContext): assert result.list is not None assert len(result.list) > 0 assert any( - "claude-sonnet-4.5" in json.dumps(model, sort_keys=True) - for model in result.list + "claude-sonnet-5" in json.dumps(model, sort_keys=True) for model in result.list ) finally: await _stop_client(client) @@ -126,7 +125,7 @@ async def test_should_add_byok_provider_and_model_at_runtime(self, ctx: E2ETestC provider=provider_name, id=model_id, name="SDK Runtime Model", - model_id="claude-sonnet-4.5", + model_id="claude-sonnet-5", wire_model="wire-sdk-runtime-model", max_context_window_tokens=4096, max_prompt_tokens=3072, diff --git a/python/e2e/test_session_config_e2e.py b/python/e2e/test_session_config_e2e.py index 62dc671893..4fc78e645d 100644 --- a/python/e2e/test_session_config_e2e.py +++ b/python/e2e/test_session_config_e2e.py @@ -167,8 +167,8 @@ def _create_anthropic_provider() -> dict: "type": "anthropic", "base_url": "https://anthropic-citations.invalid/v1", "api_key": "test-provider-key", - "model_id": "claude-sonnet-4.5", - "wire_model": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", + "wire_model": "claude-sonnet-5", } @@ -201,6 +201,7 @@ async def test_vision_disabled_then_enabled_via_setmodel(self, ctx: E2ETestConte session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, + model="claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=False) ), @@ -213,7 +214,7 @@ async def test_vision_disabled_then_enabled_via_setmodel(self, ctx: E2ETestConte # Switch vision on await session.set_model( - "claude-sonnet-4.5", + "claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=True) ), @@ -234,6 +235,7 @@ async def test_vision_enabled_then_disabled_via_setmodel(self, ctx: E2ETestConte session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, + model="claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=True) ), @@ -246,7 +248,7 @@ async def test_vision_enabled_then_disabled_via_setmodel(self, ctx: E2ETestConte # Switch vision off await session.set_model( - "claude-sonnet-4.5", + "claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=False) ), @@ -295,7 +297,7 @@ async def test_should_forward_clientname_in_useragent(self, ctx: E2ETestContext) async def test_should_forward_custom_provider_headers_on_create(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider=_make_proxy_provider(ctx.proxy_url, "create-provider-header"), ) @@ -319,7 +321,7 @@ async def test_should_forward_custom_provider_headers_on_resume(self, ctx: E2ETe session2 = await ctx.client.resume_session( session_id, on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider=_make_proxy_provider(ctx.proxy_url, "resume-provider-header"), ) @@ -345,7 +347,7 @@ async def test_should_forward_provider_wire_model(self, ctx: E2ETestContext): # it directly (see unit tests for serialization coverage). session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider={ "type": "openai", "base_url": ctx.proxy_url, @@ -374,7 +376,7 @@ async def test_should_use_provider_model_id_as_wire_model(self, ctx: E2ETestCont "type": "openai", "base_url": ctx.proxy_url, "api_key": "test-provider-key", - "model_id": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", }, ) @@ -382,7 +384,7 @@ async def test_should_use_provider_model_id_as_wire_model(self, ctx: E2ETestCont exchanges = await ctx.get_exchanges() assert len(exchanges) == 1 - assert exchanges[0]["request"]["model"] == "claude-sonnet-4.5" + assert exchanges[0]["request"]["model"] == "claude-sonnet-5" await session.disconnect() @@ -473,7 +475,7 @@ async def test_should_enable_citations_for_anthropic_file_attachments_on_create( try: session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", enable_citations=True, provider=_create_anthropic_provider(), ) @@ -521,7 +523,7 @@ async def test_should_enable_citations_for_anthropic_file_attachments_on_resume( session2 = await resume_client.resume_session( session1.session_id, on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", enable_citations=True, provider=_create_anthropic_provider(), ) diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py index 08413f228f..a126eabc40 100644 --- a/python/e2e/test_session_e2e.py +++ b/python/e2e/test_session_e2e.py @@ -25,7 +25,7 @@ class TestSessions: async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" ) assert session.session_id @@ -33,7 +33,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): assert len(messages) > 0 assert messages[0].type.value == "session.start" assert messages[0].data.session_id == session.session_id - assert messages[0].data.selected_model == "claude-sonnet-4.5" + assert messages[0].data.selected_model == "claude-sonnet-5" await session.disconnect() diff --git a/rust/tests/e2e/client.rs b/rust/tests/e2e/client.rs index 880c1a28c4..3abedce1a8 100644 --- a/rust/tests/e2e/client.rs +++ b/rust/tests/e2e/client.rs @@ -120,7 +120,7 @@ async fn should_list_models_when_authenticated() { let models = client.list_models().await.expect("list models"); assert!( - models.iter().any(|model| model.id == "claude-sonnet-4.5"), + models.iter().any(|model| model.id == "claude-sonnet-5"), "expected default replay model in {models:?}" ); diff --git a/rust/tests/e2e/client_options.rs b/rust/tests/e2e/client_options.rs index fc1ceebb83..fa8e6f68f7 100644 --- a/rust/tests/e2e/client_options.rs +++ b/rust/tests/e2e/client_options.rs @@ -27,7 +27,7 @@ async fn should_forward_advanced_session_creation_options_to_the_cli() { SessionConfig::default() .with_session_id("advanced-session-id") .with_client_name("rust-sdk-e2e-client") - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_reasoning_effort("low") .with_reasoning_summary(ReasoningSummary::None) .with_context_tier("long_context") @@ -90,7 +90,7 @@ async fn should_forward_advanced_session_creation_options_to_the_cli() { [ ("sessionId", json!("advanced-session-id")), ("clientName", json!("rust-sdk-e2e-client")), - ("model", json!("claude-sonnet-4.5")), + ("model", json!("claude-sonnet-5")), ("reasoningEffort", json!("low")), ("reasoningSummary", json!("none")), ("contextTier", json!("long_context")), diff --git a/rust/tests/e2e/copilot_request_handler.rs b/rust/tests/e2e/copilot_request_handler.rs index 46b4e510cd..478845f48e 100644 --- a/rust/tests/e2e/copilot_request_handler.rs +++ b/rust/tests/e2e/copilot_request_handler.rs @@ -101,8 +101,8 @@ fn sse(event_type: &str, data: &Value) -> String { fn model_catalog(supported_endpoints: Option<&[&str]>) -> String { let mut model = json!({ - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -110,7 +110,7 @@ fn model_catalog(supported_endpoints: Option<&[&str]>) -> String { "model_picker_enabled": true, "capabilities": { "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": { "max_context_window_tokens": 200000, @@ -232,7 +232,7 @@ fn synth_inference_response(url: &str, body: &[u8], text: &str) -> CopilotHttpRe "id": "chatcmpl-stub-1", "object": "chat.completion.chunk", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", }) }; let mut c1 = base(); @@ -257,7 +257,7 @@ fn synth_inference_response(url: &str, body: &[u8], text: &str) -> CopilotHttpRe "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "choices": [{ "index": 0, "message": { "role": "assistant", "content": text }, @@ -668,14 +668,14 @@ async fn threads_session_id_into_inference() { let before = handler.inference_records().len(); let byok_config = SessionConfig::default() .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_provider( ProviderConfig::new("https://byok.invalid/v1") .with_provider_type("openai") .with_wire_api("responses") .with_api_key("byok-secret") - .with_model_id("claude-sonnet-4.5") - .with_wire_model("claude-sonnet-4.5"), + .with_model_id("claude-sonnet-5") + .with_wire_model("claude-sonnet-5"), ); let byok_session = client .create_session(byok_config) diff --git a/rust/tests/e2e/rewind.rs b/rust/tests/e2e/rewind.rs index 44d6a39404..485c389ece 100644 --- a/rust/tests/e2e/rewind.rs +++ b/rust/tests/e2e/rewind.rs @@ -28,7 +28,7 @@ async fn should_restore_tracked_file_and_conversation() { let session = client .create_session( ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_enable_file_change_tracking(true), ) .await diff --git a/rust/tests/e2e/rpc_server.rs b/rust/tests/e2e/rpc_server.rs index 8df662ea8e..2e80ae1d70 100644 --- a/rust/tests/e2e/rpc_server.rs +++ b/rust/tests/e2e/rpc_server.rs @@ -70,7 +70,7 @@ async fn should_call_rpc_models_list_with_typed_result() { result .models .iter() - .any(|model| model.id == "claude-sonnet-4.5") + .any(|model| model.id == "claude-sonnet-5") ); assert!(result.models.iter().all(|model| !model.name.is_empty())); client.stop().await.expect("stop client"); diff --git a/rust/tests/e2e/rpc_session_state.rs b/rust/tests/e2e/rpc_session_state.rs index c723d10f5e..e6d6a2b546 100644 --- a/rust/tests/e2e/rpc_session_state.rs +++ b/rust/tests/e2e/rpc_session_state.rs @@ -27,7 +27,7 @@ use super::support::{ assistant_message_content, recv_with_timeout, wait_for_condition, wait_for_event, }; -const MODEL_ID: &str = "claude-sonnet-4.5"; +const MODEL_ID: &str = "claude-sonnet-5"; #[tokio::test] async fn should_call_session_rpc_model_getcurrent() { diff --git a/rust/tests/e2e/rpc_session_state_extras.rs b/rust/tests/e2e/rpc_session_state_extras.rs index 81901d06ae..85c20debc7 100644 --- a/rust/tests/e2e/rpc_session_state_extras.rs +++ b/rust/tests/e2e/rpc_session_state_extras.rs @@ -12,7 +12,7 @@ use github_copilot_sdk::session_events::PermissionMode; use super::support::{assistant_message_content, with_e2e_context}; -const MODEL_ID: &str = "claude-sonnet-4.5"; +const MODEL_ID: &str = "claude-sonnet-5"; #[tokio::test] async fn should_list_models_for_session() { diff --git a/rust/tests/e2e/session.rs b/rust/tests/e2e/session.rs index 8c2463a5fa..04c99422ea 100644 --- a/rust/tests/e2e/session.rs +++ b/rust/tests/e2e/session.rs @@ -37,7 +37,7 @@ async fn shouldcreateanddisconnectsessions() { let session = client .create_session( ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5"), + .with_model("claude-sonnet-5"), ) .await .expect("create session"); diff --git a/rust/tests/e2e/session_config.rs b/rust/tests/e2e/session_config.rs index c3f6b57aea..2c844a8415 100644 --- a/rust/tests/e2e/session_config.rs +++ b/rust/tests/e2e/session_config.rs @@ -362,7 +362,7 @@ fn anthropic_message_stream_body(text: &str) -> String { "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [], "stop_reason": null, "stop_sequence": null, @@ -414,8 +414,8 @@ fn synth_non_inference_response(url: &str) -> CopilotHttpResponse { json_headers(), json!({ "data": [{ - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -423,7 +423,7 @@ fn synth_non_inference_response(url: &str) -> CopilotHttpResponse { "model_picker_enabled": true, "capabilities": { "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": { "max_context_window_tokens": 200000, @@ -459,7 +459,7 @@ fn synth_inference_response(url: &str, body: &[u8]) -> CopilotHttpResponse { "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [{ "type": "text", "text": SYNTHETIC_TEXT }], "stop_reason": "end_turn", "stop_sequence": null, @@ -474,7 +474,7 @@ fn synth_inference_response(url: &str, body: &[u8]) -> CopilotHttpResponse { "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "choices": [{ "index": 0, "message": { "role": "assistant", "content": SYNTHETIC_TEXT }, @@ -489,8 +489,8 @@ fn anthropic_provider() -> ProviderConfig { ProviderConfig::new("https://anthropic-citations.invalid/v1") .with_provider_type("anthropic") .with_api_key("test-provider-key") - .with_model_id("claude-sonnet-4.5") - .with_wire_model("claude-sonnet-4.5") + .with_model_id("claude-sonnet-5") + .with_wire_model("claude-sonnet-5") } fn pdf_attachment() -> Attachment { @@ -532,7 +532,7 @@ async fn should_enable_citations_for_anthropic_file_attachments_on_create() { .create_session( SessionConfig::default() .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_enable_citations(true) .with_provider(anthropic_provider()), ) @@ -592,7 +592,7 @@ async fn should_enable_citations_for_anthropic_file_attachments_on_resume() { .resume_session( ResumeSessionConfig::new(session1.id().clone()) .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_enable_citations(true) .with_provider(anthropic_provider()), ) diff --git a/test/harness/modelProtocolAdapters.test.ts b/test/harness/modelProtocolAdapters.test.ts index c0ed084150..add59ae77f 100644 --- a/test/harness/modelProtocolAdapters.test.ts +++ b/test/harness/modelProtocolAdapters.test.ts @@ -42,7 +42,7 @@ const endpoints: Record = { const models: Record = { capi: "gpt-4.1", - "anthropic-messages": "claude-sonnet-4.5", + "anthropic-messages": "claude-sonnet-5", "openai-responses": "gpt-4.1", "openai-completions": "gpt-4.1", }; diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index 245b035c5c..9933a746d9 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -1591,6 +1591,42 @@ Always include PINEAPPLE_COCONUT_42. } }); + test.each([false, true])( + "defaults to Sonnet 5 without stored models (capture exists: %s)", + async (captureExists) => { + const cachePath = path.join(tempDir, "cache.yaml"); + if (captureExists) { + await writeFile( + cachePath, + yaml.stringify({ + models: [], + conversations: [], + } satisfies NormalizedData), + ); + } + + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/models", { + method: "GET", + }); + expect(response.status).toBe(200); + const parsed = JSON.parse(response.body) as { + data: Array<{ id: string }>; + }; + expect(parsed.data.map((model) => model.id)).toEqual(["claude-sonnet-5"]); + } finally { + await proxy.stop(); + } + }, + ); + test("returns cached models for /models endpoint", async () => { const cachePath = path.join(tempDir, "cache.yaml"); const cacheContent = yaml.stringify({ diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index f30a9fbd77..7da04cabc4 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -107,7 +107,7 @@ const normalizedToolNames: Record = { * Default model to use when no stored data is available for a given test. * This enables responding to /models without needing to have a capture file. */ -const defaultModel = "claude-sonnet-4.5"; +const defaultModel = "claude-sonnet-5"; /** * An HTTP proxy that not only captures HTTP exchanges, but also stores them in a file on disk and diff --git a/test/snapshots/abort/should_abort_during_active_streaming.yaml b/test/snapshots/abort/should_abort_during_active_streaming.yaml index 70981ee597..8556fec349 100644 --- a/test/snapshots/abort/should_abort_during_active_streaming.yaml +++ b/test/snapshots/abort/should_abort_during_active_streaming.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/abort/should_abort_during_active_tool_execution.yaml b/test/snapshots/abort/should_abort_during_active_tool_execution.yaml index 99ea89f7b0..a975cae284 100644 --- a/test/snapshots/abort/should_abort_during_active_tool_execution.yaml +++ b/test/snapshots/abort/should_abort_during_active_tool_execution.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml b/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml index ac5cc94336..498bfebad0 100644 --- a/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml +++ b/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml b/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml index 4ba16d4d81..1624cb5317 100644 --- a/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml +++ b/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml b/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml index 49944c9732..c33ce7e8f2 100644 --- a/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml +++ b/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml b/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml index 4549b99dc1..417f0b3446 100644 --- a/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml +++ b/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml b/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml index 705378061f..97e08e5852 100644 --- a/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml +++ b/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml b/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml index 01cf1298d3..5c3c638d50 100644 --- a/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml +++ b/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_capture_stderr_output.yaml b/test/snapshots/builtin_tools/should_capture_stderr_output.yaml index 0ba318148d..ba0bd164ea 100644 --- a/test/snapshots/builtin_tools/should_capture_stderr_output.yaml +++ b/test/snapshots/builtin_tools/should_capture_stderr_output.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_create_a_new_file.yaml b/test/snapshots/builtin_tools/should_create_a_new_file.yaml index 8afe8b38b6..869777e9a4 100644 --- a/test/snapshots/builtin_tools/should_create_a_new_file.yaml +++ b/test/snapshots/builtin_tools/should_create_a_new_file.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml b/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml index 3f4e986906..922d7751dd 100644 --- a/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml +++ b/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml b/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml index 6cf85ea51d..338dd03ef2 100644 --- a/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml +++ b/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml b/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml index c5c00fb65c..410da4e089 100644 --- a/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml +++ b/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml b/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml index 601ae0f04c..23a7fec7a1 100644 --- a/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml +++ b/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml b/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml index f0af500b6e..615b9ae39e 100644 --- a/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml +++ b/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml b/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml +++ b/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml b/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml +++ b/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_force_stop_client.yaml b/test/snapshots/client/should_force_stop_client.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_force_stop_client.yaml +++ b/test/snapshots/client/should_force_stop_client.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_get_authenticated_status.yaml b/test/snapshots/client/should_get_authenticated_status.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_get_authenticated_status.yaml +++ b/test/snapshots/client/should_get_authenticated_status.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_get_status.yaml b/test/snapshots/client/should_get_status.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_get_status.yaml +++ b/test/snapshots/client/should_get_status.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_list_models_when_authenticated.yaml b/test/snapshots/client/should_list_models_when_authenticated.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_list_models_when_authenticated.yaml +++ b/test/snapshots/client/should_list_models_when_authenticated.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml b/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml +++ b/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml b/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml +++ b/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_stop_client_with_active_session.yaml b/test/snapshots/client/should_stop_client_with_active_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_stop_client_with_active_session.yaml +++ b/test/snapshots/client/should_stop_client_with_active_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client_api/should_delete_session_by_id.yaml b/test/snapshots/client_api/should_delete_session_by_id.yaml index 0981462bf6..bfeaca5f6d 100644 --- a/test/snapshots/client_api/should_delete_session_by_id.yaml +++ b/test/snapshots/client_api/should_delete_session_by_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml b/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml index 8486832a46..8e3aa9d94c 100644 --- a/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml +++ b/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml b/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml index beb8b443d2..3569a8ca8e 100644 --- a/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml +++ b/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml b/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml index 4419c5854e..bb4a148072 100644 --- a/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml +++ b/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml b/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml index 3b9da534c2..62da2b03a2 100644 --- a/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml +++ b/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml b/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml +++ b/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml b/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml index 469d091288..c87d0cb124 100644 --- a/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml +++ b/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml b/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml index 60d1eadeaf..51716b5c80 100644 --- a/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/commands/session_with_commands_creates_successfully.yaml b/test/snapshots/commands/session_with_commands_creates_successfully.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/commands/session_with_commands_creates_successfully.yaml +++ b/test/snapshots/commands/session_with_commands_creates_successfully.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/commands/session_with_commands_resumes_successfully.yaml b/test/snapshots/commands/session_with_commands_resumes_successfully.yaml index 0981462bf6..bfeaca5f6d 100644 --- a/test/snapshots/commands/session_with_commands_resumes_successfully.yaml +++ b/test/snapshots/commands/session_with_commands_resumes_successfully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml b/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml +++ b/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml b/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml index 9773a132f5..6d966efe2f 100644 --- a/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml +++ b/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml index 9deca12228..7d476ec66c 100644 --- a/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml +++ b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml b/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml +++ b/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml b/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml index 16db486e88..e454b1e96c 100644 --- a/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml +++ b/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml b/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml +++ b/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml b/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml +++ b/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml b/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml +++ b/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml b/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml +++ b/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml b/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml +++ b/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/input_returns_freeform_value.yaml b/test/snapshots/elicitation/input_returns_freeform_value.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/input_returns_freeform_value.yaml +++ b/test/snapshots/elicitation/input_returns_freeform_value.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/select_returns_selected_option.yaml b/test/snapshots/elicitation/select_returns_selected_option.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/select_returns_selected_option.yaml +++ b/test/snapshots/elicitation/select_returns_selected_option.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml b/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml +++ b/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml b/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml +++ b/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml b/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml +++ b/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml b/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml index caac261e2a..1499c083ca 100644 --- a/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml +++ b/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml b/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml index 48667da723..af30539431 100644 --- a/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml +++ b/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml b/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml index c8f272e6b9..726cbf9322 100644 --- a/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml +++ b/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml b/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml index ecc10bdbd6..2b24bf8a50 100644 --- a/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml +++ b/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml b/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml index 48667da723..af30539431 100644 --- a/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml +++ b/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml b/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml index 46fd7715ab..717f09ea24 100644 --- a/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml +++ b/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml b/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml index 48667da723..af30539431 100644 --- a/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml +++ b/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml b/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml index 1797cc16b1..6fcaebc6a0 100644 --- a/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml +++ b/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml b/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml index 10bca8e4b0..296ee0db9a 100644 --- a/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml +++ b/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml b/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml index 9ed9431545..10e3a06189 100644 --- a/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml +++ b/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml b/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml index 2a5f1ae446..3bf5ee1f4d 100644 --- a/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml b/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml index f695c60f3d..86c2865756 100644 --- a/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml b/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml index 2860f52c15..a1ecaef30e 100644 --- a/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml +++ b/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml index 9ed9431545..10e3a06189 100644 --- a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml +++ b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml index 9ed9431545..10e3a06189 100644 --- a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml +++ b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml b/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml index 2a5f1ae446..3bf5ee1f4d 100644 --- a/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml b/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml index f695c60f3d..86c2865756 100644 --- a/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml b/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml index a340e9326c..f5cbaff9c6 100644 --- a/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml +++ b/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml b/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml index 8415fe771b..89c3ee9234 100644 --- a/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml +++ b/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml b/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml index 6485670a1c..6c1a055c04 100644 --- a/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml +++ b/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml b/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml index dcd2f32be2..2a17b8a0a3 100644 --- a/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml b/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml index bde2373cc2..bd61da81cb 100644 --- a/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml b/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml index 0d2da93e50..e200c7f68f 100644 --- a/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml b/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml index beb8b443d2..3569a8ca8e 100644 --- a/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml b/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml index bbe815735c..a1c3f4fb2d 100644 --- a/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml +++ b/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml b/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml index 75fccc4e1f..017000cfba 100644 --- a/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml +++ b/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml b/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml index c1d643b4c3..725222bfd3 100644 --- a/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml +++ b/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -13,7 +13,7 @@ conversations: Hi! 👋 - I'm GitHub Copilot CLI, powered by claude-sonnet-4.5. I'm here to help you with software engineering tasks + I'm GitHub Copilot CLI, powered by claude-sonnet-5. I'm here to help you with software engineering tasks like exploring codebases, running commands, making code changes, and more. diff --git a/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml b/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml index ceb291c9dd..688dc01e1e 100644 --- a/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml +++ b/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml b/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml index db2b029680..1fdf62b876 100644 --- a/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml +++ b/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml b/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml index 2f02a0570d..ef34d886b1 100644 --- a/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml +++ b/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml b/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml index 9703495c66..dbc06f70b1 100644 --- a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml index 82c9917c34..96321ffe3a 100644 --- a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml +++ b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml index 16db486e88..e454b1e96c 100644 --- a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml +++ b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml b/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml b/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml index 60d1eadeaf..51716b5c80 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml b/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml index 60d1eadeaf..51716b5c80 100644 --- a/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml index 9703495c66..dbc06f70b1 100644 --- a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml index 65fe6664e6..5fab1b6334 100644 --- a/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml b/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml b/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml b/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml index f5506bb184..bf8c7b4d33 100644 --- a/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml +++ b/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml b/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml index 29ba0fc68b..b3eab15f66 100644 --- a/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml +++ b/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml b/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml index c1df8e8023..9aa12434df 100644 --- a/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml +++ b/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml b/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml +++ b/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml b/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml index 82c9917c34..96321ffe3a 100644 --- a/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml +++ b/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml b/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml index fac88270d5..6193c536b4 100644 --- a/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml +++ b/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml b/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml index decf64bc37..b6606842af 100644 --- a/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml +++ b/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml b/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml index decf64bc37..b6606842af 100644 --- a/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml +++ b/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml b/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml index decf64bc37..b6606842af 100644 --- a/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml +++ b/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml b/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml index 6f23714d94..58ec8e370a 100644 --- a/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml +++ b/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml b/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml index 5d63a94018..cfa604adbe 100644 --- a/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml +++ b/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml b/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml index 19c271b4f1..30fee89306 100644 --- a/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml +++ b/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml @@ -1,8 +1,8 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 - auto errors: - - model: claude-sonnet-4.5 + - model: claude-sonnet-5 status: 429 code: user_weekly_rate_limited message: You've reached your weekly rate limit. diff --git a/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml b/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml index 078ba05483..9ee28ad083 100644 --- a/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml +++ b/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml b/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml index 62f0d004a0..89f28fcbe1 100644 --- a/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml +++ b/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml b/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml index b6410e0d1b..925e8f3076 100644 --- a/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml +++ b/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml b/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml index 2397bfdc01..fce177c1c3 100644 --- a/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml +++ b/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml b/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml index ba9db87d08..105a472f9c 100644 --- a/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml +++ b/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml b/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml index c04864d827..39d56792d5 100644 --- a/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml +++ b/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml b/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml +++ b/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml b/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml index 583366363a..7d991ce94e 100644 --- a/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml +++ b/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml b/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml index 96dc365c65..3ea91fec16 100644 --- a/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml +++ b/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml b/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml index acae9a8b24..29574ef20b 100644 --- a/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml +++ b/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml b/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml index f9fcc188a7..c39ad4f6e4 100644 --- a/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml +++ b/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml b/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml index 4856cdc4c6..4e001d5cf2 100644 --- a/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml +++ b/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml index 8a32e431a7..d8ffe5df61 100644 --- a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml +++ b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml index 4ae08f8a80..85098c636c 100644 --- a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml +++ b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml b/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml index 3b5c7dfe41..bef3a92876 100644 --- a/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml +++ b/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml b/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml index d7117cee65..de0c2634b1 100644 --- a/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml +++ b/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml b/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml +++ b/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml b/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml +++ b/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml b/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml +++ b/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml b/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml +++ b/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/permissions/async_permission_handler.yaml b/test/snapshots/permissions/async_permission_handler.yaml index 1d46c38a41..bf3431703c 100644 --- a/test/snapshots/permissions/async_permission_handler.yaml +++ b/test/snapshots/permissions/async_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/deny_permission.yaml b/test/snapshots/permissions/deny_permission.yaml index 480b640fb3..63ed54f671 100644 --- a/test/snapshots/permissions/deny_permission.yaml +++ b/test/snapshots/permissions/deny_permission.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/permission_handler_errors.yaml b/test/snapshots/permissions/permission_handler_errors.yaml index cee78a0929..f8f274d66e 100644 --- a/test/snapshots/permissions/permission_handler_errors.yaml +++ b/test/snapshots/permissions/permission_handler_errors.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/permission_handler_for_shell_commands.yaml b/test/snapshots/permissions/permission_handler_for_shell_commands.yaml index 1d46c38a41..bf3431703c 100644 --- a/test/snapshots/permissions/permission_handler_for_shell_commands.yaml +++ b/test/snapshots/permissions/permission_handler_for_shell_commands.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/permission_handler_for_write_operations.yaml b/test/snapshots/permissions/permission_handler_for_write_operations.yaml index 3f05a8c6de..9811c4a6ab 100644 --- a/test/snapshots/permissions/permission_handler_for_write_operations.yaml +++ b/test/snapshots/permissions/permission_handler_for_write_operations.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/resume_session_with_permission_handler.yaml b/test/snapshots/permissions/resume_session_with_permission_handler.yaml index 6296a0d73e..ade442a927 100644 --- a/test/snapshots/permissions/resume_session_with_permission_handler.yaml +++ b/test/snapshots/permissions/resume_session_with_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml b/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml index ef6f60dbed..8e28d8156d 100644 --- a/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml +++ b/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml b/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml index 0dfbd9e6b2..5f9a98ac80 100644 --- a/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml +++ b/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml index 0d25979c7c..0f7c4782cc 100644 --- a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml +++ b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml index 9a23c55f0a..fc802f16da 100644 --- a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml +++ b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_handle_async_permission_handler.yaml b/test/snapshots/permissions/should_handle_async_permission_handler.yaml index 1d46c38a41..bf3431703c 100644 --- a/test/snapshots/permissions/should_handle_async_permission_handler.yaml +++ b/test/snapshots/permissions/should_handle_async_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml b/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml index 924123536c..7432c4caf9 100644 --- a/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml +++ b/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml b/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml index 2a2db62101..17b75a4925 100644 --- a/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml +++ b/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml b/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml index ef6f60dbed..8e28d8156d 100644 --- a/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml +++ b/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml b/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml index 3f05a8c6de..9811c4a6ab 100644 --- a/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml +++ b/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml b/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml index 90407df6fc..cd73f7e165 100644 --- a/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml +++ b/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml b/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml index 6296a0d73e..ade442a927 100644 --- a/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml +++ b/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml b/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml index 3a6d66dc8d..1c33d19a98 100644 --- a/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml +++ b/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml b/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml index 19398ce5d6..9ecbdb6a51 100644 --- a/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml +++ b/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml b/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml index 9199977dba..554b4b1f70 100644 --- a/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml +++ b/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml b/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml index 90407df6fc..cd73f7e165 100644 --- a/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml +++ b/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml b/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml index c771647842..5e1970247a 100644 --- a/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml +++ b/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml b/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml index d7ff876a6c..b8d0e30d19 100644 --- a/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml +++ b/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml b/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml index 1d92fe8eed..cf77c42735 100644 --- a/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml +++ b/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml b/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml +++ b/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml b/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml index 8a099ec46a..31753aed11 100644 --- a/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml +++ b/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml b/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml +++ b/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml b/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml +++ b/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml b/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml +++ b/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml b/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml +++ b/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml b/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml +++ b/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml b/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml +++ b/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_call_agent_reload.yaml b/test/snapshots/rpc_agents/should_call_agent_reload.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_call_agent_reload.yaml +++ b/test/snapshots/rpc_agents/should_call_agent_reload.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_deselect_current_agent.yaml b/test/snapshots/rpc_agents/should_deselect_current_agent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_deselect_current_agent.yaml +++ b/test/snapshots/rpc_agents/should_deselect_current_agent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml b/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml +++ b/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml b/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml +++ b/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml b/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml +++ b/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml b/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml +++ b/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml b/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml +++ b/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml b/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml index 7c58a8da96..200cc28dba 100644 --- a/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml +++ b/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml b/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml b/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml index 7c58a8da96..200cc28dba 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml b/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml b/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml b/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml b/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml +++ b/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml b/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml +++ b/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml b/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml b/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml b/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml b/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml b/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml b/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml +++ b/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml b/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml +++ b/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml b/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml +++ b/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml b/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml +++ b/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml b/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml +++ b/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml b/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml +++ b/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml b/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml +++ b/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml b/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml +++ b/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml b/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml +++ b/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml b/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml +++ b/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml b/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml +++ b/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml b/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml +++ b/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml b/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml +++ b/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml b/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml +++ b/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml b/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml +++ b/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml b/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml +++ b/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml b/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml +++ b/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml b/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml +++ b/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml b/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml +++ b/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml b/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml +++ b/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml b/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml +++ b/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml b/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml index b276b6a398..9011c931e7 100644 --- a/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml +++ b/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml @@ -1,4 +1,4 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 - gpt-5.4 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml b/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml +++ b/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml b/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml +++ b/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml b/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml index 001e828461..ad474d2472 100644 --- a/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml +++ b/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml b/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml +++ b/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml b/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml +++ b/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml b/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml index 76ba212c5e..c1223c8a20 100644 --- a/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml +++ b/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml b/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml index 2313bd1483..f6fc12e544 100644 --- a/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml +++ b/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml b/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml +++ b/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml b/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml +++ b/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml b/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml +++ b/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml b/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml +++ b/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml b/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml +++ b/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml b/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml +++ b/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml b/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml +++ b/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml b/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml index 788c5b75f2..e59f571b4a 100644 --- a/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml +++ b/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml b/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml +++ b/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml b/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml index d79fd22384..6ea625da4d 100644 --- a/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml +++ b/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml b/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml +++ b/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml b/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml +++ b/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml b/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml +++ b/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml b/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml b/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml index c4798dc83d..24c4d6bbf2 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml b/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml index 73f0499002..33bb479064 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml b/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml b/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml +++ b/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml b/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml +++ b/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml b/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml +++ b/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml b/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml +++ b/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml b/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml +++ b/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml b/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml +++ b/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml b/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml +++ b/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml b/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml +++ b/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml b/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml +++ b/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml b/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml index 65ced1e366..d2d8a272c1 100644 --- a/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml +++ b/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml b/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml b/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml b/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml +++ b/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml b/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml +++ b/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml b/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml b/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml b/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml index 41bbe583d2..a9dbdd3751 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml b/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml index fec44be1fe..c5c1ce1c11 100644 --- a/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml +++ b/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml b/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml index 20eefc57a9..8bfc5f5053 100644 --- a/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml +++ b/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml b/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml index 192105ac7a..476d5aeb1c 100644 --- a/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml +++ b/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml b/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml index c97e969df6..57bc48df32 100644 --- a/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml +++ b/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml b/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml +++ b/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml b/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml +++ b/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml b/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml +++ b/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml b/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml index c2e705ed2d..4ef08ab255 100644 --- a/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml +++ b/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml b/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml +++ b/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/sendandwait_throws_on_timeout.yaml b/test/snapshots/session/sendandwait_throws_on_timeout.yaml index 0e019bdad7..fc736fe650 100644 --- a/test/snapshots/session/sendandwait_throws_on_timeout.yaml +++ b/test/snapshots/session/sendandwait_throws_on_timeout.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml b/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml index a03140fa17..7e1a256daa 100644 --- a/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml +++ b/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_abort_a_session.yaml b/test/snapshots/session/should_abort_a_session.yaml index dbbbd32aa7..4ec09b51f3 100644 --- a/test/snapshots/session/should_abort_a_session.yaml +++ b/test/snapshots/session/should_abort_a_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_accept_blob_attachments.yaml b/test/snapshots/session/should_accept_blob_attachments.yaml index 1cca7142db..4caf7c8707 100644 --- a/test/snapshots/session/should_accept_blob_attachments.yaml +++ b/test/snapshots/session/should_accept_blob_attachments.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml index 3bf4a39f05..a0a9f1c391 100644 --- a/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml +++ b/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_availabletools.yaml b/test/snapshots/session/should_create_a_session_with_availabletools.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_a_session_with_availabletools.yaml +++ b/test/snapshots/session/should_create_a_session_with_availabletools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml index f3ce077a62..50d9cbecfa 100644 --- a/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml +++ b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -8,8 +8,8 @@ conversations: content: Who are you? - role: assistant content: >- - I'm **GitHub Copilot CLI**, a terminal assistant built by GitHub. I'm powered by claude-sonnet-4.5 (model ID: - claude-sonnet-4.5). + I'm **GitHub Copilot CLI**, a terminal assistant built by GitHub. I'm powered by claude-sonnet-5 (model ID: + claude-sonnet-5). I'm here to help you with software engineering tasks, including: diff --git a/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml b/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml +++ b/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_excludedtools.yaml b/test/snapshots/session/should_create_a_session_with_excludedtools.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_a_session_with_excludedtools.yaml +++ b/test/snapshots/session/should_create_a_session_with_excludedtools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml index ebe0881b90..88d25dc817 100644 --- a/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml +++ b/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_session_with_custom_config_dir.yaml b/test/snapshots/session/should_create_session_with_custom_config_dir.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_session_with_custom_config_dir.yaml +++ b/test/snapshots/session/should_create_session_with_custom_config_dir.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_session_with_custom_tool.yaml b/test/snapshots/session/should_create_session_with_custom_tool.yaml index 4ae6dab721..3a08cbd9fe 100644 --- a/test/snapshots/session/should_create_session_with_custom_tool.yaml +++ b/test/snapshots/session/should_create_session_with_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_delete_session.yaml b/test/snapshots/session/should_delete_session.yaml index fb8249d325..72176060ce 100644 --- a/test/snapshots/session/should_delete_session.yaml +++ b/test/snapshots/session/should_delete_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_get_last_session_id.yaml b/test/snapshots/session/should_get_last_session_id.yaml index 3b9da534c2..62da2b03a2 100644 --- a/test/snapshots/session/should_get_last_session_id.yaml +++ b/test/snapshots/session/should_get_last_session_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_get_session_metadata.yaml b/test/snapshots/session/should_get_session_metadata.yaml index b326528e1d..f9b44b833a 100644 --- a/test/snapshots/session/should_get_session_metadata.yaml +++ b/test/snapshots/session/should_get_session_metadata.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_get_session_metadata_by_id.yaml b/test/snapshots/session/should_get_session_metadata_by_id.yaml index b326528e1d..f9b44b833a 100644 --- a/test/snapshots/session/should_get_session_metadata_by_id.yaml +++ b/test/snapshots/session/should_get_session_metadata_by_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_have_stateful_conversation.yaml b/test/snapshots/session/should_have_stateful_conversation.yaml index 39d3c5acc5..1309304fc4 100644 --- a/test/snapshots/session/should_have_stateful_conversation.yaml +++ b/test/snapshots/session/should_have_stateful_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_list_sessions.yaml b/test/snapshots/session/should_list_sessions.yaml index 4683506570..d2c3ebdd4f 100644 --- a/test/snapshots/session/should_list_sessions.yaml +++ b/test/snapshots/session/should_list_sessions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_list_sessions_with_context.yaml b/test/snapshots/session/should_list_sessions_with_context.yaml index 8486832a46..8e3aa9d94c 100644 --- a/test/snapshots/session/should_list_sessions_with_context.yaml +++ b/test/snapshots/session/should_list_sessions_with_context.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_log_messages_at_various_levels.yaml b/test/snapshots/session/should_log_messages_at_various_levels.yaml index 0e019bdad7..fc736fe650 100644 --- a/test/snapshots/session/should_log_messages_at_various_levels.yaml +++ b/test/snapshots/session/should_log_messages_at_various_levels.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_receive_session_events.yaml b/test/snapshots/session/should_receive_session_events.yaml index 229563a4cf..406c34f455 100644 --- a/test/snapshots/session/should_receive_session_events.yaml +++ b/test/snapshots/session/should_receive_session_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml b/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml index bd02858372..667d0c3528 100644 --- a/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml +++ b/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml b/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml index b012e26ea8..ff54ee73da 100644 --- a/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml +++ b/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_custom_requestheaders.yaml b/test/snapshots/session/should_send_with_custom_requestheaders.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session/should_send_with_custom_requestheaders.yaml +++ b/test/snapshots/session/should_send_with_custom_requestheaders.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_directory_attachment.yaml b/test/snapshots/session/should_send_with_directory_attachment.yaml index aa410c9295..f2e8835c5d 100644 --- a/test/snapshots/session/should_send_with_directory_attachment.yaml +++ b/test/snapshots/session/should_send_with_directory_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_file_attachment.yaml b/test/snapshots/session/should_send_with_file_attachment.yaml index 2e8e4d1d2d..f2acea13fb 100644 --- a/test/snapshots/session/should_send_with_file_attachment.yaml +++ b/test/snapshots/session/should_send_with_file_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_github_reference_attachment.yaml b/test/snapshots/session/should_send_with_github_reference_attachment.yaml index 6e298de554..851f34c50f 100644 --- a/test/snapshots/session/should_send_with_github_reference_attachment.yaml +++ b/test/snapshots/session/should_send_with_github_reference_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_mode_property.yaml b/test/snapshots/session/should_send_with_mode_property.yaml index 4fec86c7f4..e6639acbdb 100644 --- a/test/snapshots/session/should_send_with_mode_property.yaml +++ b/test/snapshots/session/should_send_with_mode_property.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_selection_attachment.yaml b/test/snapshots/session/should_send_with_selection_attachment.yaml index ad6a2a28e8..c46f3d679b 100644 --- a/test/snapshots/session/should_send_with_selection_attachment.yaml +++ b/test/snapshots/session/should_send_with_selection_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_set_model_on_existing_session.yaml b/test/snapshots/session/should_set_model_on_existing_session.yaml index 0e019bdad7..fc736fe650 100644 --- a/test/snapshots/session/should_set_model_on_existing_session.yaml +++ b/test/snapshots/session/should_set_model_on_existing_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_set_model_with_reasoningeffort.yaml b/test/snapshots/session/should_set_model_with_reasoningeffort.yaml index ccf204d2ae..db151e253a 100644 --- a/test/snapshots/session/should_set_model_with_reasoningeffort.yaml +++ b/test/snapshots/session/should_set_model_with_reasoningeffort.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 - gpt-5.4 conversations: - messages: diff --git a/test/snapshots/session_config/should_accept_blob_attachments.yaml b/test/snapshots/session_config/should_accept_blob_attachments.yaml index 672ca74d4e..71c7900348 100644 --- a/test/snapshots/session_config/should_accept_blob_attachments.yaml +++ b/test/snapshots/session_config/should_accept_blob_attachments.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_accept_message_attachments.yaml b/test/snapshots/session_config/should_accept_message_attachments.yaml index 5525d1fb04..188905752b 100644 --- a/test/snapshots/session_config/should_accept_message_attachments.yaml +++ b/test/snapshots/session_config/should_accept_message_attachments.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml b/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml +++ b/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml b/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml index 3cbf86e981..4ef99a4d36 100644 --- a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml +++ b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml b/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml +++ b/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml b/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml +++ b/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml b/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml b/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml +++ b/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_apply_session_limits_on_create.yaml b/test/snapshots/session_config/should_apply_session_limits_on_create.yaml index 904d69c872..142c4f6229 100644 --- a/test/snapshots/session_config/should_apply_session_limits_on_create.yaml +++ b/test/snapshots/session_config/should_apply_session_limits_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml b/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml index 904d69c872..142c4f6229 100644 --- a/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml b/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml index a455f6f6f3..a5111fd56a 100644 --- a/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml b/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml index 9d3dd78ff1..ec7d207e2e 100644 --- a/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml b/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml +++ b/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml b/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml +++ b/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml b/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml +++ b/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml b/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml +++ b/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml b/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml +++ b/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_use_custom_session_id.yaml b/test/snapshots/session_config/should_use_custom_session_id.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_use_custom_session_id.yaml +++ b/test/snapshots/session_config/should_use_custom_session_id.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml b/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml +++ b/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml b/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml index 40000d491b..dd2b4592b3 100644 --- a/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml +++ b/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml b/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml index 1eb0acd729..a4000a80e6 100644 --- a/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml +++ b/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml b/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml index b987a4e630..6afc03cb05 100644 --- a/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml +++ b/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml b/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml index 4744667cd7..2d2d1e38ae 100644 --- a/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml +++ b/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml b/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml index e80ce51e64..9b8f1eb0d9 100644 --- a/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml +++ b/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml b/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml index 5b0e81b22d..90ceee231a 100644 --- a/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml +++ b/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml b/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml index 269a80f11a..9aba0fefe1 100644 --- a/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml +++ b/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml b/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml index 455652bfd8..086c69225d 100644 --- a/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml +++ b/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml b/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml index 5e62634668..67f8e0121e 100644 --- a/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml +++ b/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml b/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml index 0a0325417c..59ed4624c7 100644 --- a/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml +++ b/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml b/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml index 32b8bc354f..b2b50348f5 100644 --- a/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml +++ b/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml b/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml index b26cfbfebe..e51bc5d247 100644 --- a/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml +++ b/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml b/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml index b302f117d4..6e38f93031 100644 --- a/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml +++ b/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml b/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml index f4add013a1..47bd9bc305 100644 --- a/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml +++ b/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml b/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml index 274ab7d2fa..f39bcb95cd 100644 --- a/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml +++ b/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml b/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml index fd621f2b0b..005604fdd6 100644 --- a/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml +++ b/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml b/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml index fdb7ebca03..180fb52406 100644 --- a/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml +++ b/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml b/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml index 425c772fce..0a4b41c9f2 100644 --- a/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml +++ b/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml b/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml index 007c5c1c53..fcecde4286 100644 --- a/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml +++ b/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml b/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml index 38b35946b0..ee9e6452d2 100644 --- a/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml +++ b/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml b/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml index 0c678deab8..1cc5776a34 100644 --- a/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml +++ b/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml b/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml index 0c678deab8..1cc5776a34 100644 --- a/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml +++ b/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml b/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml index 3b18558220..c6d74510f4 100644 --- a/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml +++ b/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml b/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml index 25e10c4b12..ca514e0135 100644 --- a/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml +++ b/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml b/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml index d210f22ea0..c9c9d1200f 100644 --- a/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml +++ b/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml b/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml index 3b18558220..c6d74510f4 100644 --- a/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml +++ b/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml b/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml index 25e10c4b12..ca514e0135 100644 --- a/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml +++ b/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml b/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml index f920f8705b..4c5419a0ab 100644 --- a/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml +++ b/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml b/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml index c033a6cba1..5230710868 100644 --- a/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml +++ b/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml b/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml index 97939357ca..54e64b3edb 100644 --- a/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml +++ b/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml b/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml index 32e07aa5d5..3bf332978d 100644 --- a/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml +++ b/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml b/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml index a3a35bf25b..d853722e5b 100644 --- a/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml +++ b/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml b/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml index 5721955d27..e53acdf159 100644 --- a/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml +++ b/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml b/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml index 9d2c688c18..f73112abb5 100644 --- a/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml +++ b/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -13,5 +13,5 @@ conversations: watering, pests, plant identification, or growing tips, I'm here to help! - I'm powered by claude-sonnet-4.5, but I focus specifically on gardening topics. What plant or gardening + I'm powered by claude-sonnet-5, but I focus specifically on gardening topics. What plant or gardening question can I help you with today? diff --git a/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml index 98e57919c6..4db03cac93 100644 --- a/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml +++ b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml index c54f25e2aa..93b7221aa3 100644 --- a/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml +++ b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml index 32d6367390..94c50ff0bd 100644 --- a/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml +++ b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml b/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml index f8342047b7..c919c8d0b1 100644 --- a/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml +++ b/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml b/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml index 621dfc4e8d..0992eae919 100644 --- a/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml +++ b/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml b/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml index 6a829fb23f..3658586343 100644 --- a/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml +++ b/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml b/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml index 3fddb1600b..b328558471 100644 --- a/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml +++ b/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml b/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml index 891f75cb55..d329be915e 100644 --- a/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml +++ b/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml b/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml index 2388d7d8fd..6c8ee43bb9 100644 --- a/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml +++ b/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml b/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml index 71021d3b8d..bc65d05d6b 100644 --- a/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml +++ b/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/can_receive_and_return_complex_types.yaml b/test/snapshots/tools/can_receive_and_return_complex_types.yaml index be869484ea..982f66e3ad 100644 --- a/test/snapshots/tools/can_receive_and_return_complex_types.yaml +++ b/test/snapshots/tools/can_receive_and_return_complex_types.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml b/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml index 36d5adce4a..39d2bed2f0 100644 --- a/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml +++ b/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml b/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml index 47f9286e0c..59acd092aa 100644 --- a/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml +++ b/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/ergonomic_tool_arity0.yaml b/test/snapshots/tools/ergonomic_tool_arity0.yaml index a55f486816..16bbbf1cd6 100644 --- a/test/snapshots/tools/ergonomic_tool_arity0.yaml +++ b/test/snapshots/tools/ergonomic_tool_arity0.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/ergonomic_tool_arity2.yaml b/test/snapshots/tools/ergonomic_tool_arity2.yaml index e34c695bd4..b6cdd07614 100644 --- a/test/snapshots/tools/ergonomic_tool_arity2.yaml +++ b/test/snapshots/tools/ergonomic_tool_arity2.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/ergonomic_tool_definition.yaml b/test/snapshots/tools/ergonomic_tool_definition.yaml index ebb05ce1b9..e61e95f930 100644 --- a/test/snapshots/tools/ergonomic_tool_definition.yaml +++ b/test/snapshots/tools/ergonomic_tool_definition.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/handles_tool_calling_errors.yaml b/test/snapshots/tools/handles_tool_calling_errors.yaml index 33226722dd..3b85da9acd 100644 --- a/test/snapshots/tools/handles_tool_calling_errors.yaml +++ b/test/snapshots/tools/handles_tool_calling_errors.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/invokes_built_in_tools.yaml b/test/snapshots/tools/invokes_built_in_tools.yaml index 0fba134424..72d959be46 100644 --- a/test/snapshots/tools/invokes_built_in_tools.yaml +++ b/test/snapshots/tools/invokes_built_in_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/invokes_custom_tool.yaml b/test/snapshots/tools/invokes_custom_tool.yaml index 6f212e4a79..6362a5e4f7 100644 --- a/test/snapshots/tools/invokes_custom_tool.yaml +++ b/test/snapshots/tools/invokes_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml b/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml index fcb6fa7266..2cfccba989 100644 --- a/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml +++ b/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/low_level_tool_definition.yaml b/test/snapshots/tools/low_level_tool_definition.yaml index 03cb0748a2..5921d061cb 100644 --- a/test/snapshots/tools/low_level_tool_definition.yaml +++ b/test/snapshots/tools/low_level_tool_definition.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml b/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml index 5410d3f295..ec2fa0a9a7 100644 --- a/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml +++ b/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml b/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml index a9aae3aea5..949ffd18fa 100644 --- a/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml +++ b/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml b/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml index cf0cf564da..50494f2324 100644 --- a/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml +++ b/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml b/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml index dfdfa63fa7..fa7a5cfbe1 100644 --- a/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml +++ b/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system From c1a7d5b022662a4cd4078979ef31147ac60549c8 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 2 Sep 2026 06:58:07 -0700 Subject: [PATCH 29/60] Fix unused variable warning on Windows in rust/build/in_process.rs (#2471) The `executable: bool` parameter of `install_cached_file_path` is only read inside a `#[cfg(unix)]` block (used to chmod extracted binaries), so it is reported as unused on non-Unix targets. This fails downstream consumers building the crate from main with `RUSTFLAGS=-D warnings`. Add an explicit `#[cfg(not(unix))] let _ = executable;` to mark the parameter used on those targets, without changing Unix behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- rust/build/in_process.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rust/build/in_process.rs b/rust/build/in_process.rs index f1e947b27a..e6edcf1b90 100644 --- a/rust/build/in_process.rs +++ b/rust/build/in_process.rs @@ -574,6 +574,12 @@ fn install_cached_file_path( bytes: &[u8], executable: bool, ) { + // `executable` only affects file permissions on Unix (see the `#[cfg(unix)]` + // block below); explicitly mark it used elsewhere so non-Unix targets don't + // warn about an unused parameter under `-D warnings`. + #[cfg(not(unix))] + let _ = executable; + assert!( !relative_path.is_absolute() && !relative_path.components().any(|component| { From 7d86829be93cc4cd7ca3668ae56da1def45af2a2 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 2 Sep 2026 16:56:24 +0100 Subject: [PATCH 30/60] Reduce merge queue flakiness (#2484) * ci: reduce merge queue flakiness Contain hanging .NET test processes while preserving diagnostics, make Java native artifacts stable across rerun attempts, retry transient package downloads, and allow slower Rust proxy startup on loaded runners. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(test): prevent replay proxy teardown hangs Force-close active proxy connections and upstream requests during shutdown so fixture disposal cannot wait forever after a streaming test. Cover pending requests and streaming responses with regression tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: preserve existing .NET timeout budgets Keep the existing job and hang-detection timeouts while retaining diagnostic output for genuine failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: run harness regression tests in CI Execute the harness suite once in the Node workflow and include it in the repository-wide test target so proxy shutdown coverage gates changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: isolate replay writes from CI environment Replay proxy unit tests verify snapshot serialization, so preserve and clear the ambient GITHUB_ACTIONS flag around each test instead of suppressing their output files in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dotnet-sdk-tests.yml | 20 +++++++++- .github/workflows/java-sdk-tests.yml | 25 ++++++++---- .github/workflows/nodejs-sdk-tests.yml | 5 +++ .github/workflows/python-sdk-tests.yml | 4 +- justfile | 8 +++- rust/tests/e2e/support.rs | 5 ++- test/harness/capturingHttpProxy.test.ts | 42 ++++++++++++++++++++ test/harness/capturingHttpProxy.ts | 53 +++++++++++++++++++++++-- test/harness/replayingCapiProxy.test.ts | 8 ++++ 9 files changed, 151 insertions(+), 19 deletions(-) diff --git a/.github/workflows/dotnet-sdk-tests.yml b/.github/workflows/dotnet-sdk-tests.yml index 6ac2a0d7c9..2a446c97be 100644 --- a/.github/workflows/dotnet-sdk-tests.yml +++ b/.github/workflows/dotnet-sdk-tests.yml @@ -175,7 +175,16 @@ jobs: # whole suite normally finishes in about five. # The validation job performs the full analyzer-enabled SDK build. # Build only test-consumed frameworks here and do not repeat analyzers. - args=(--no-restore -v n --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type none -p:RunAnalyzers=false) + args=( + --no-restore + -v n + --blame-hang + --blame-hang-timeout 10m + --blame-hang-dump-type none + --logger "trx;LogFilePrefix=test-results" + --results-directory "$GITHUB_WORKSPACE/dotnet/TestResults" + -p:RunAnalyzers=false + ) filter="$DOTNET_TEST_FILTER" if [[ "$DOTNET_TEST_SHARD" != "full" ]]; then @@ -217,3 +226,12 @@ jobs: args+=(--filter "$filter") fi dotnet test test/GitHub.Copilot.SDK.Test.csproj "${args[@]}" + + - name: Upload .NET test diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: dotnet-test-diagnostics-${{ matrix.os }}-${{ matrix.transport }}-${{ matrix.backend }}-${{ matrix.shard }}-${{ github.run_attempt }} + path: dotnet/TestResults/ + if-no-files-found: warn + retention-days: 7 diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index 321c5c4f55..88185deb14 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -7,6 +7,11 @@ on: permissions: contents: read +env: + MAVEN_OPTS: >- + -Daether.connector.http.retryHandler.count=3 + -Daether.connector.http.retryHandler.serviceUnavailable=429,502,503 + jobs: java-sdk-inprocess: name: "Java SDK InProcess Tests (${{ matrix.classifier }})" @@ -121,11 +126,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-linux-arm64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linux-arm64.jar java/copilot-native/target/linux-arm64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-windows: @@ -178,11 +184,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-win32-x64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-x64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-x64.jar java/copilot-native/target/win32-x64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-windows-arm64: @@ -235,11 +242,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-win32-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-arm64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-arm64.jar java/copilot-native/target/win32-arm64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-darwin: @@ -293,11 +301,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-darwin-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-darwin-arm64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-darwin-arm64.jar java/copilot-native/target/darwin-arm64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-assembly: @@ -333,22 +342,22 @@ jobs: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-linux-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/linux-arm64 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-win32-x64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-x64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/windows - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-win32-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/windows-arm64 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-darwin-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-darwin-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/darwin - name: Verify native inputs and deploy the complete local release diff --git a/.github/workflows/nodejs-sdk-tests.yml b/.github/workflows/nodejs-sdk-tests.yml index 7647bf35d1..1738828e6b 100644 --- a/.github/workflows/nodejs-sdk-tests.yml +++ b/.github/workflows/nodejs-sdk-tests.yml @@ -53,6 +53,11 @@ jobs: working-directory: ./test/harness run: npm ci --ignore-scripts + - name: Run test harness tests + if: runner.os == 'Linux' && matrix.transport == 'default' + working-directory: ./test/harness + run: npm test + - name: Warm up PowerShell if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" diff --git a/.github/workflows/python-sdk-tests.yml b/.github/workflows/python-sdk-tests.yml index 3c0644a7c0..aa6e803372 100644 --- a/.github/workflows/python-sdk-tests.yml +++ b/.github/workflows/python-sdk-tests.yml @@ -50,7 +50,7 @@ jobs: - name: Install Node.js dependencies (for CLI in tests) working-directory: ./nodejs - run: npm ci --ignore-scripts + run: npm ci --ignore-scripts --fetch-retries=4 --fetch-retry-mintimeout=10000 --fetch-retry-maxtimeout=60000 - name: Run ruff format check run: uv run ruff format --check . @@ -63,7 +63,7 @@ jobs: - name: Install test harness dependencies working-directory: ./test/harness - run: npm ci --ignore-scripts + run: npm ci --ignore-scripts --fetch-retries=4 --fetch-retry-mintimeout=10000 --fetch-retry-maxtimeout=60000 - name: Warm up PowerShell if: runner.os == 'Windows' diff --git a/justfile b/justfile index c84166862f..69666bc00a 100644 --- a/justfile +++ b/justfile @@ -9,7 +9,7 @@ format: format-go format-python format-nodejs format-dotnet format-rust lint: lint-go lint-python lint-nodejs lint-dotnet lint-rust # Run tests for all languages -test: test-go test-python test-nodejs test-dotnet test-rust test-corrections +test: test-go test-python test-nodejs test-dotnet test-rust test-harness test-corrections # Format Go code format-go: @@ -66,6 +66,11 @@ test-nodejs: @echo "=== Testing Node.js code ===" @cd nodejs && npm test +# Run test harness tests +test-harness: + @echo "=== Testing test harness ===" + @cd test/harness && npm test + # Test .NET code test-dotnet: @echo "=== Testing .NET code ===" @@ -168,4 +173,3 @@ validate-docs-go: validate-docs-cs: @echo "=== Validating C# documentation ===" @cd scripts/docs-validation && npm run validate:cs - diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index 8a4161efef..42accf2ec7 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -31,6 +31,7 @@ static SHARED_E2E_RUNTIME: LazyLock = LazyLock::new(|| .expect("create shared E2E runtime") }); const SHARED_E2E_CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); +const PROXY_STARTUP_TIMEOUT: Duration = Duration::from_secs(30); pub const DEFAULT_TEST_TOKEN: &str = "rust-e2e-token"; @@ -1274,7 +1275,7 @@ impl CapiProxy { } }); let re = regex::Regex::new(r"Listening: (http://[^\s]+)\s+(\{.*\})$").unwrap(); - let deadline = Instant::now() + SHARED_E2E_CLEANUP_TIMEOUT; + let deadline = Instant::now() + PROXY_STARTUP_TIMEOUT; while let Some(remaining) = deadline.checked_duration_since(Instant::now()) { let line = match line_rx.recv_timeout(remaining) { Ok(Ok(line)) => line, @@ -1341,7 +1342,7 @@ impl CapiProxy { kill_and_wait_child(&mut child); Err(std::io::Error::other(format!( - "timed out after {SHARED_E2E_CLEANUP_TIMEOUT:?} waiting for proxy startup" + "timed out after {PROXY_STARTUP_TIMEOUT:?} waiting for proxy startup" ))) } diff --git a/test/harness/capturingHttpProxy.test.ts b/test/harness/capturingHttpProxy.test.ts index f434d3e67c..cba4d45e53 100644 --- a/test/harness/capturingHttpProxy.test.ts +++ b/test/harness/capturingHttpProxy.test.ts @@ -10,9 +10,21 @@ describe("Capturing HTTP Proxy", () => { let proxy: CapturingHttpProxy; let testServer: http.Server; let testServerAddress: string; + let onHangingRequest: (() => void) | undefined; + let onStreamingResponse: (() => void) | undefined; beforeEach(async () => { testServer = http.createServer((req, res) => { + if (req.url === "/hang") { + onHangingRequest?.(); + return; + } + if (req.url === "/stream") { + res.writeHead(200, { "content-type": "text/plain" }); + res.write("started"); + onStreamingResponse?.(); + return; + } res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ message: "Hello", path: req.url })); }); @@ -71,4 +83,34 @@ describe("Capturing HTTP Proxy", () => { } as CapturedExchange, ]); }); + + test("stops while a proxied request is still active", async () => { + proxy = new CapturingHttpProxy(testServerAddress); + const proxyUrl = await proxy.start(); + const requestStarted = new Promise((resolve) => { + onHangingRequest = resolve; + }); + const request = fetch(`${proxyUrl}/hang`).catch(() => undefined); + await requestStarted; + + await proxy.stop(); + + await request; + }); + + test("stops while a proxied response is still streaming", async () => { + proxy = new CapturingHttpProxy(testServerAddress); + const proxyUrl = await proxy.start(); + const responseStarted = new Promise((resolve) => { + onStreamingResponse = resolve; + }); + const responsePromise = fetch(`${proxyUrl}/stream`); + await responseStarted; + const response = await responsePromise; + const body = response.text().catch(() => undefined); + + await proxy.stop(); + + await body; + }); }); diff --git a/test/harness/capturingHttpProxy.ts b/test/harness/capturingHttpProxy.ts index edccca4ead..fdc1fc46c1 100644 --- a/test/harness/capturingHttpProxy.ts +++ b/test/harness/capturingHttpProxy.ts @@ -10,7 +10,10 @@ import https from "https"; */ export class CapturingHttpProxy { private readonly capturedExchanges: CapturedExchange[] = []; + private readonly activeRequests = new Set(); + private readonly activeResponses = new Set(); private server?: http.Server; + private stopPromise?: Promise; constructor(private targetUrl: string) {} @@ -90,6 +93,10 @@ export class CapturingHttpProxy { res.end(); }, onError: (err) => { + if (!this.server) { + res.destroy(); + return; + } console.error("Error in proxying request:", err); const endTime = Date.now(); const formattedError = @@ -130,9 +137,19 @@ export class CapturingHttpProxy { } async stop(): Promise { - if (this.server) { - return new Promise((resolve, reject) => { - this.server!.close((err) => { + if (this.stopPromise) { + return this.stopPromise; + } + + const server = this.server; + if (!server) { + return; + } + + this.server = undefined; + this.stopPromise = (async () => { + const closed = new Promise((resolve, reject) => { + server.close((err) => { if (err) { reject(err); } else { @@ -140,14 +157,38 @@ export class CapturingHttpProxy { } }); }); - } + + // server.close() waits for active connections. A replayed streaming request + // can otherwise wedge fixture teardown after its test has already passed. + server.closeAllConnections(); + for (const response of this.activeResponses) { + response.destroy(); + } + this.activeResponses.clear(); + for (const request of this.activeRequests) { + request.destroy(); + } + this.activeRequests.clear(); + + await closed; + })(); + return this.stopPromise; } performRequest(options: PerformRequestOptions): void { + if (this.stopPromise) { + options.onError(new Error("Proxy is stopping")); + return; + } + const protocol = options.isHttps ? https : http; const upstreamRequest = protocol.request( options.requestOptions, (upstreamResponse) => { + this.activeResponses.add(upstreamResponse); + upstreamResponse.once("close", () => { + this.activeResponses.delete(upstreamResponse); + }); options.onResponseStart( upstreamResponse.statusCode || 500, upstreamResponse.headers, @@ -157,6 +198,10 @@ export class CapturingHttpProxy { }, ); + this.activeRequests.add(upstreamRequest); + upstreamRequest.once("close", () => { + this.activeRequests.delete(upstreamRequest); + }); upstreamRequest.on("error", options.onError); if (options.body) { diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index 9933a746d9..2aa535e798 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -24,13 +24,21 @@ import { ShellConfig } from "./util"; describe("ReplayingCapiProxy", () => { let tempDir: string; let workDir: string; + let githubActions: string | undefined; beforeEach(async () => { + githubActions = process.env.GITHUB_ACTIONS; + delete process.env.GITHUB_ACTIONS; tempDir = await mkdtemp(path.join(os.tmpdir(), "capi-proxy-test-")); workDir = path.join(tempDir, "work"); }); afterEach(async () => { + if (githubActions === undefined) { + delete process.env.GITHUB_ACTIONS; + } else { + process.env.GITHUB_ACTIONS = githubActions; + } await rm(tempDir, { recursive: true, force: true }); }); From 5725e40e58a9373bfc499f5e4f36fab79b935153 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:18:32 +0000 Subject: [PATCH 31/60] Update @github/copilot to 1.0.83-2 (#2483) * Update @github/copilot to 1.0.83-2 - Updated nodejs and test harness dependencies - Re-ran code generators - Formatted generated code * Fix Java tests for generated schema changes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix Rust checks for generated schema changes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Split hanging macOS .NET test shard Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Further isolate macOS .NET tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Isolate macOS RPC test groups Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fully isolate macOS .NET test groups Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Isolate flaky .NET test hosts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Split macOS MCP test hosts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Dispose MCP test sessions promptly Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Stephen Toub Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dotnet-sdk-tests.yml | 96 ++- dotnet/src/Generated/Rpc.cs | 637 +++++++++++++---- dotnet/src/Generated/SessionEvents.cs | 668 +++++++++++++++--- dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs | 20 +- go/rpc/zrpc.go | 207 +++++- go/rpc/zsession_encoding.go | 16 + go/rpc/zsession_events.go | 157 +++- go/zsession_events.go | 24 + java/pom.xml | 2 +- java/scripts/codegen/package-lock.json | 72 +- java/scripts/codegen/package.json | 2 +- .../copilot/generated/AgentModelPolicy.java | 35 + .../AssistantFusionPhaseActivityEvent.java | 57 ++ .../CompletionReceiptEventRange.java | 29 + .../generated/CompletionReceiptFinalTool.java | 33 + .../CompletionReceiptStopReason.java | 37 + .../CompletionReceiptToolStatus.java | 41 ++ .../generated/CustomAgentsUpdatedAgent.java | 8 +- .../generated/FusionPhaseActivityKind.java | 37 + .../generated/FusionPhasePlanStep.java | 33 + .../generated/PermissionRequestedEvent.java | 2 + .../SessionCompletionReceiptEvent.java | 55 ++ .../copilot/generated/SessionEvent.java | 4 + .../generated/SessionFusionResolvedEvent.java | 3 + .../generated/SubagentCompletedEvent.java | 2 + .../generated/SubagentFailedEvent.java | 2 + .../copilot/generated/UserMessageEvent.java | 2 + .../copilot/generated/rpc/AgentInfo.java | 6 +- .../generated/rpc/AgentModelPolicy.java | 35 + .../copilot/generated/rpc/DiscoveredHook.java | 39 + .../copilot/generated/rpc/HookOrigin.java | 39 + .../copilot/generated/rpc/HookType.java | 2 +- .../generated/rpc/HooksDiscoverParams.java | 33 + .../generated/rpc/HooksDiscoverResult.java | 35 + .../generated/rpc/QueuePendingItems.java | 2 + .../rpc/SandboxConfigUserPolicyNetwork.java | 2 +- .../SandboxConfigUserPolicyNetworkProxy.java | 2 +- .../copilot/generated/rpc/ServerHooksApi.java | 40 ++ .../copilot/generated/rpc/ServerRpc.java | 3 + .../generated/rpc/ServerSessionsApi.java | 11 + .../rpc/SessionModelSwitchToParams.java | 2 +- .../SessionsReadPersistedEventsParams.java | 36 + .../SessionsReadPersistedEventsResult.java | 38 + .../generated/rpc/SubagentSettingsEntry.java | 2 + .../copilot/RpcSessionStateExtrasE2ETest.java | 2 +- .../rpc/GeneratedRpcRecordsCoverageTest.java | 9 +- nodejs/package-lock.json | 54 +- nodejs/package.json | 2 +- nodejs/samples/package-lock.json | 2 +- nodejs/src/generated/rpc.ts | 229 ++++-- nodejs/src/generated/session-events.ts | 289 +++++++- python/copilot/generated/rpc.py | 406 +++++++++-- python/copilot/generated/session_events.py | 289 +++++++- rust/src/generated/api_types.rs | 305 ++++++-- rust/src/generated/rpc.rs | 77 ++ rust/src/generated/session_events.rs | 348 +++++++-- rust/tests/e2e/rpc_session_state_extras.rs | 1 + scripts/codegen/rust.ts | 11 +- test/harness/package-lock.json | 54 +- test/harness/package.json | 2 +- 60 files changed, 4026 insertions(+), 662 deletions(-) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.java diff --git a/.github/workflows/dotnet-sdk-tests.yml b/.github/workflows/dotnet-sdk-tests.yml index 2a446c97be..0acf807f0a 100644 --- a/.github/workflows/dotnet-sdk-tests.yml +++ b/.github/workflows/dotnet-sdk-tests.yml @@ -104,7 +104,43 @@ jobs: - os: macos-latest transport: default backend: capi - shard: "2b" + shard: "2b1a-permission-provider" + - os: macos-latest + transport: default + backend: capi + shard: "2b1a-rewind" + - os: macos-latest + transport: default + backend: capi + shard: "2b1a-rpc-a" + - os: macos-latest + transport: default + backend: capi + shard: "2b1a-rpc-e" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-mcp-and-skills" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-mcp-config" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-mcp-lifecycle" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-queue" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-remote" + - os: macos-latest + transport: default + backend: capi + shard: "2b2" - os: macos-latest transport: default backend: capi @@ -187,6 +223,7 @@ jobs: ) filter="$DOTNET_TEST_FILTER" + individual_filters=() if [[ "$DOTNET_TEST_SHARD" != "full" ]]; then case "$DOTNET_TEST_SHARD" in 1) @@ -203,9 +240,54 @@ jobs: initials=(B E F G) shard_filter="" ;; - 2b) - initials=(O P R) + 2b1a-permission-provider) + initials=() shard_filter="" + individual_filters=( + "FullyQualifiedName~GitHub.Copilot.Test.E2E.PendingWorkResumeE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.E2E.PerSessionAuthE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.E2E.PermissionE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.E2E.PreMcpToolCallHookE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.E2E.ProviderEndpointE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.Unit.PermissionHandlerTests" + "FullyQualifiedName~GitHub.Copilot.Test.Unit.PublicDtoTests" + ) + ;; + 2b1a-rewind) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.Rewind" + ;; + 2b1a-rpc-a) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcA" + ;; + 2b1a-rpc-e) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcE" + ;; + 2b1b-mcp-and-skills) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpAndSkillsE2ETests" + ;; + 2b1b-mcp-config) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpConfigE2ETests" + ;; + 2b1b-mcp-lifecycle) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpLifecycleE2ETests" + ;; + 2b1b-queue) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcQ" + ;; + 2b1b-remote) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcR" + ;; + 2b2) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcS|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcT|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcU|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcW|FullyQualifiedName~GitHub.Copilot.Test.Unit.R" ;; 2c) initials=(T V X Z) @@ -213,6 +295,14 @@ jobs: ;; esac + if (( ${#individual_filters[@]} > 0 )); then + for individual_filter in "${individual_filters[@]}"; do + combined_filter="${filter:+(${filter})&}(${individual_filter})" + dotnet test test/GitHub.Copilot.SDK.Test.csproj "${args[@]}" --filter "$combined_filter" + done + exit 0 + fi + for namespace in E2E Unit; do for initial in "${initials[@]}"; do clause="FullyQualifiedName~GitHub.Copilot.Test.${namespace}.${initial}" diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 4379a2c122..06a717aa86 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -99,6 +99,69 @@ internal sealed class ConnectRequest public string? Token { get; set; } } +/// One server-discovered hook action from user, repository, plugin, or managed-policy configuration. +[Experimental(Diagnostics.Experimental)] +public sealed class DiscoveredHook +{ + /// Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + [JsonPropertyName("disableKey")] + public string? DisableKey { get; set; } + + /// Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Hook event that invokes this action. + [JsonPropertyName("hookType")] + public HookType HookType { get; set; } + + /// Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Configuration tier that contributed this hook action. + [JsonPropertyName("origin")] + public HookOrigin Origin { get; set; } + + /// Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + [JsonPropertyName("projectPath")] + public string? ProjectPath { get; set; } + + /// Human-readable source label, such as a hook file path, settings source, or plugin name. + [JsonPropertyName("source")] + public string? Source { get; set; } +} + +/// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. +[Experimental(Diagnostics.Experimental)] +public sealed class HooksDiscoverResult +{ + /// Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + [JsonPropertyName("errors")] + public IList Errors { get => field ??= []; set; } + + /// All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + [JsonPropertyName("hooks")] + public IList Hooks { get => field ??= []; set; } + + /// Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } +} + +/// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +[Experimental(Diagnostics.Experimental)] +internal sealed class HooksDiscoverRequest +{ + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + [JsonPropertyName("excludeHostHooks")] + public bool? ExcludeHostHooks { get; set; } + + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + [JsonPropertyName("projectPaths")] + public IList? ProjectPaths { get; set; } +} + /// Active server-driven promotion for a model, including its discount and optional expiry. [Experimental(Diagnostics.Experimental)] public sealed class ModelBillingPromo @@ -3241,7 +3304,7 @@ internal sealed class SkillsConfigSetSkillDisabledRequest public string Name { get; set; } = string.Empty; } -/// Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. +/// Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. [Experimental(Diagnostics.Experimental)] public sealed class AgentInfo { @@ -3266,6 +3329,14 @@ public sealed class AgentInfo [JsonPropertyName("model")] public string? Model { get; set; } + /// Whether authored models are preferences or required constraints. + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } + + /// Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + [JsonPropertyName("models")] + public IList? Models { get; set; } + /// Name of the agent. Use `id` as the stable selection identifier. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; @@ -4230,6 +4301,48 @@ internal sealed class SessionsGetMetadataRequest public string SessionId { get; set; } = string.Empty; } +/// Batch of session events returned by a read, with cursor and continuation metadata. +[Experimental(Diagnostics.Experimental)] +public sealed class EventsReadResult +{ + /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). + [JsonPropertyName("cursor")] + public string Cursor { get; set; } = string.Empty; + + /// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. + [JsonPropertyName("cursorStatus")] + public EventsCursorStatus CursorStatus { get; set; } + + /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. + [JsonPropertyName("events")] + public IList Events { get => field ??= []; set; } + + /// True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. + [JsonPropertyName("hasMore")] + public bool HasMore { get; set; } +} + +/// Pagination options for reading an inactive or active local session's persisted event journal. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionsReadPersistedEventsRequest +{ + /// Opaque cursor returned by a previous persisted-event read. Omit on the first call. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } + + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. + [JsonPropertyName("direction")] + public EventsReadDirection? Direction { get; set; } + + /// Maximum number of events to return in this batch (1–1000, default 200). + [JsonPropertyName("max")] + public long? Max { get; set; } + + /// Session ID whose persisted event journal should be read. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Recent local session IDs that contain user-visible history. [Experimental(Diagnostics.Experimental)] internal sealed class SessionsListNonEmptySessionIdsResult @@ -7487,7 +7600,7 @@ internal sealed class ModelSwitchToRequest [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. [JsonPropertyName("source")] public ModelChangeSource? Source { get; set; } @@ -11175,7 +11288,7 @@ public sealed class SandboxConfigUserPolicyNetworkProxy [JsonPropertyName("password")] public string? Password { get; set; } - /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. [JsonPropertyName("url")] public string Url { get; set; } = string.Empty; @@ -11196,7 +11309,7 @@ public sealed class SandboxConfigUserPolicyNetwork [JsonPropertyName("allowOutbound")] public bool? AllowOutbound { get; set; } - /// HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. + /// HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is accepted and routed through the IPv4 gateway), and does not support proxy credentials. macOS relies on applications honoring proxy environment variables. Windows also configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's networking stack. Configure supported credentials in the separate `username` and `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, while an https:// or authenticated loopback URL uses the URL form. [JsonPropertyName("proxy")] public SandboxConfigUserPolicyNetworkProxy? Proxy { get; set; } } @@ -12966,6 +13079,10 @@ public sealed class SubagentSettingsEntry /// Model override for matching subagents. [JsonPropertyName("model")] public string? Model { get; set; } + + /// Whether the configured model strategy is preferred or required. + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } } /// Configured per-agent subagent overrides. @@ -16683,6 +16800,10 @@ public sealed class QueuePendingItems /// Whether this item is a queued user message or a queued slash command / model change. [JsonPropertyName("kind")] public QueuePendingItemsKind Kind { get; set; } + + /// Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + [JsonPropertyName("messageId")] + public string? MessageId { get; set; } } /// Snapshot of the session's pending queued items and immediate-steering messages. @@ -17105,27 +17226,6 @@ internal sealed class SessionQueueProcessRequest public string SessionId { get; set; } = string.Empty; } -/// Batch of session events returned by a read, with cursor and continuation metadata. -[Experimental(Diagnostics.Experimental)] -public sealed class EventsReadResult -{ - /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). - [JsonPropertyName("cursor")] - public string Cursor { get; set; } = string.Empty; - - /// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. - [JsonPropertyName("cursorStatus")] - public EventsCursorStatus CursorStatus { get; set; } - - /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. - [JsonPropertyName("events")] - public IList Events { get => field ??= []; set; } - - /// True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. - [JsonPropertyName("hasMore")] - public bool HasMore { get; set; } -} - /// Cursor, batch size, and optional long-poll/filter parameters for reading session events. [Experimental(Diagnostics.Experimental)] internal sealed class EventLogReadRequest @@ -18837,6 +18937,183 @@ public sealed class GitHubTokenAcquireRequest public string? SessionId { get; set; } } +/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Runs before a tool is invoked. + public static HookType PreToolUse { get; } = new("preToolUse"); + + /// Runs before an MCP tool is invoked. + public static HookType PreMcpToolCall { get; } = new("preMcpToolCall"); + + /// Runs after a tool completes successfully. + public static HookType PostToolUse { get; } = new("postToolUse"); + + /// Runs after a tool fails. + public static HookType PostToolUseFailure { get; } = new("postToolUseFailure"); + + /// Runs after the user submits a prompt. + public static HookType UserPromptSubmitted { get; } = new("userPromptSubmitted"); + + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + public static HookType UserPromptTransformed { get; } = new("userPromptTransformed"); + + /// Runs when a session starts. + public static HookType SessionStart { get; } = new("sessionStart"); + + /// Runs when a session ends. + public static HookType SessionEnd { get; } = new("sessionEnd"); + + /// Runs after an agent result is produced. + public static HookType PostResult { get; } = new("postResult"); + + /// Runs before a pull request description is generated. + public static HookType PrePRDescription { get; } = new("prePRDescription"); + + /// Runs when the agent encounters an error. + public static HookType ErrorOccurred { get; } = new("errorOccurred"); + + /// Runs when the agent stops. + public static HookType AgentStop { get; } = new("agentStop"); + + /// Runs when a subagent starts. + public static HookType SubagentStart { get; } = new("subagentStart"); + + /// Runs when a subagent stops. + public static HookType SubagentStop { get; } = new("subagentStop"); + + /// Runs before conversation context is compacted. + public static HookType PreCompact { get; } = new("preCompact"); + + /// Runs when the agent requests permission. + public static HookType PermissionRequest { get; } = new("permissionRequest"); + + /// Runs when the agent emits a notification. + public static HookType Notification { get; } = new("notification"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookType left, HookType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookType left, HookType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookType other && Equals(other); + + /// + public bool Equals(HookType 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 HookType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookType)); + } + } +} + + +/// Configuration tier that contributed a discovered hook action. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookOrigin : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookOrigin(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Hook loaded from user settings or the user's hook directory. + public static HookOrigin User { get; } = new("user"); + + /// Hook loaded from repository settings or the repository hook directory. + public static HookOrigin Repository { get; } = new("repository"); + + /// Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. + public static HookOrigin Plugin { get; } = new("plugin"); + + /// Hook enforced by centrally managed policy. + public static HookOrigin Policy { get; } = new("policy"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookOrigin left, HookOrigin right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookOrigin left, HookOrigin right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookOrigin other && Equals(other); + + /// + public bool Equals(HookOrigin 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 HookOrigin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookOrigin value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookOrigin)); + } + } +} + + /// Resolved Anthropic adaptive-thinking capability for a model. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -22329,6 +22606,132 @@ public override void Write(Utf8JsonWriter writer, SessionSource value, JsonSeria } +/// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history (the beginning for a forward read, the tail for a backward read). The fallback page is a fresh boundary snapshot, not a continuation of the requested cursor, so it may overlap already-rendered events; on 'expired' a consumer should reset/rebase its pagination state (or deduplicate by event id) before continuing from the returned cursor. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct EventsCursorStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public EventsCursorStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The cursor was applied successfully. + public static EventsCursorStatus Ok { get; } = new("ok"); + + /// The cursor referred to history that is no longer available. + public static EventsCursorStatus Expired { get; } = new("expired"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EventsCursorStatus left, EventsCursorStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EventsCursorStatus left, EventsCursorStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); + + /// + public bool Equals(EventsCursorStatus 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 EventsCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, EventsCursorStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); + } + } +} + + +/// Direction to page through the session's persisted event history. 'forward' pages from the cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct EventsReadDirection : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public EventsReadDirection(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Page from the cursor toward newer events (default). + public static EventsReadDirection Forward { get; } = new("forward"); + + /// Tail-first: return the newest events and page toward older events. + public static EventsReadDirection Backward { get; } = new("backward"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EventsReadDirection left, EventsReadDirection right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EventsReadDirection left, EventsReadDirection right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EventsReadDirection other && Equals(other); + + /// + public bool Equals(EventsReadDirection 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 EventsReadDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, EventsReadDirection value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); + } + } +} + + /// Kind of attention required when status === "attention". Meaningful only when status === "attention". [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -28619,69 +29022,6 @@ public override void Write(Utf8JsonWriter writer, QueuePendingItemsKind value, J } -/// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history (the beginning for a forward read, the tail for a backward read). The fallback page is a fresh boundary snapshot, not a continuation of the requested cursor, so it may overlap already-rendered events; on 'expired' a consumer should reset/rebase its pagination state (or deduplicate by event id) before continuing from the returned cursor. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct EventsCursorStatus : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public EventsCursorStatus(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// The cursor was applied successfully. - public static EventsCursorStatus Ok { get; } = new("ok"); - - /// The cursor referred to history that is no longer available. - public static EventsCursorStatus Expired { get; } = new("expired"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EventsCursorStatus left, EventsCursorStatus right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EventsCursorStatus left, EventsCursorStatus right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); - - /// - public bool Equals(EventsCursorStatus 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 EventsCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, EventsCursorStatus value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); - } - } -} - - /// Agent-scope filter: 'primary' returns only main-agent events plus events whose type starts with 'subagent.' (matching the typed-subscription default behavior); 'all' returns events from all agents (matching wildcard-subscription behavior). Default is 'all' to preserve wildcard semantics for catch-up callers. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -28745,69 +29085,6 @@ public override void Write(Utf8JsonWriter writer, EventsAgentScope value, JsonSe } -/// Direction to page through the session's persisted event history. 'forward' pages from the cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct EventsReadDirection : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public EventsReadDirection(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Page from the cursor toward newer events (default). - public static EventsReadDirection Forward { get; } = new("forward"); - - /// Tail-first: return the newest events and page toward older events. - public static EventsReadDirection Backward { get; } = new("backward"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EventsReadDirection left, EventsReadDirection right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EventsReadDirection left, EventsReadDirection right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is EventsReadDirection other && Equals(other); - - /// - public bool Equals(EventsReadDirection 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 EventsReadDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, EventsReadDirection value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); - } - } -} - - /// Client population used for the prediction baseline. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -29624,6 +29901,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell await CopilotClient.InvokeRpcAsync(_rpc, "registerExtensionLaunchProvider", [], cancellationToken); } + /// Hooks APIs. + public ServerHooksApi Hooks => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// Models APIs. public ServerModelsApi Models => field ?? @@ -29739,6 +30022,29 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell field; } +/// Provides server-scoped Hooks APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class ServerHooksApi +{ + private readonly JsonRpc _rpc; + + internal ServerHooksApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + /// The to monitor for cancellation requests. The default is . + /// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + public async Task DiscoverAsync(IList? projectPaths = null, bool? excludeHostHooks = null, CancellationToken cancellationToken = default) + { + var request = new HooksDiscoverRequest { ProjectPaths = projectPaths, ExcludeHostHooks = excludeHostHooks }; + return await CopilotClient.InvokeRpcAsync(_rpc, "hooks.discover", [request], cancellationToken); + } +} + /// Provides server-scoped Models APIs. [Experimental(Diagnostics.Experimental)] public sealed class ServerModelsApi @@ -30682,6 +30988,21 @@ internal async Task GetMetadataAsync(string sessionId return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.getMetadata", [request], cancellationToken); } + /// Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session. + /// Session ID whose persisted event journal should be read. + /// Opaque cursor returned by a previous persisted-event read. Omit on the first call. + /// Maximum number of events to return in this batch (1–1000, default 200). + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. + /// The to monitor for cancellation requests. The default is . + /// Batch of session events returned by a read, with cursor and continuation metadata. + public async Task ReadPersistedEventsAsync(string sessionId, string? cursor = null, long? max = null, EventsReadDirection? direction = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionId); + + var request = new SessionsReadPersistedEventsRequest { SessionId = sessionId, Cursor = cursor, Max = max, Direction = direction }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.readPersistedEvents", [request], cancellationToken); + } + /// Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. /// Maximum number of session IDs to return. /// The to monitor for cancellation requests. The default is . @@ -31996,7 +32317,7 @@ public async Task GetCurrentAsync(CancellationToken cancellationTo /// Output verbosity level to request for supported models. /// Override individual model capabilities resolved by the runtime. /// Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. /// When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. /// When true, evaluate context-window compaction policy before applying the switch. @@ -35692,6 +36013,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedCancelPhase), TypeInfoPropertyName = "SessionEventsAgentInterruptedCancelPhase")] [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedData), TypeInfoPropertyName = "SessionEventsAgentInterruptedData")] [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedEvent), TypeInfoPropertyName = "SessionEventsAgentInterruptedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.AgentModelPolicy), TypeInfoPropertyName = "SessionEventsAgentModelPolicy")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseActivityEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseActivityEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseCompletedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseFailedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseFailedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseStartedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseStartedEvent")] @@ -35797,6 +36120,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.CompactionCompleteCompactionTokensUsed), TypeInfoPropertyName = "SessionEventsCompactionCompleteCompactionTokensUsed")] [JsonSerializable(typeof(GitHub.Copilot.CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail), TypeInfoPropertyName = "SessionEventsCompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail")] [JsonSerializable(typeof(GitHub.Copilot.CompactionTrigger), TypeInfoPropertyName = "SessionEventsCompactionTrigger")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptEventRange), TypeInfoPropertyName = "SessionEventsCompletionReceiptEventRange")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptFinalTool), TypeInfoPropertyName = "SessionEventsCompletionReceiptFinalTool")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptStopReason), TypeInfoPropertyName = "SessionEventsCompletionReceiptStopReason")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptToolStatus), TypeInfoPropertyName = "SessionEventsCompletionReceiptToolStatus")] [JsonSerializable(typeof(GitHub.Copilot.ContextTier), TypeInfoPropertyName = "SessionEventsContextTier")] [JsonSerializable(typeof(GitHub.Copilot.CustomAgentsUpdatedAgent), TypeInfoPropertyName = "SessionEventsCustomAgentsUpdatedAgent")] [JsonSerializable(typeof(GitHub.Copilot.ElicitationCompletedAction), TypeInfoPropertyName = "SessionEventsElicitationCompletedAction")] @@ -35834,7 +36161,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpAction), TypeInfoPropertyName = "SessionEventsFusionFollowUpAction")] [JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpRecommendation), TypeInfoPropertyName = "SessionEventsFusionFollowUpRecommendation")] [JsonSerializable(typeof(GitHub.Copilot.FusionPattern), TypeInfoPropertyName = "SessionEventsFusionPattern")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPhaseActivityKind), TypeInfoPropertyName = "SessionEventsFusionPhaseActivityKind")] [JsonSerializable(typeof(GitHub.Copilot.FusionPhaseKind), TypeInfoPropertyName = "SessionEventsFusionPhaseKind")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPhasePlanStep), TypeInfoPropertyName = "SessionEventsFusionPhasePlanStep")] [JsonSerializable(typeof(GitHub.Copilot.FusionPhaseStatus), TypeInfoPropertyName = "SessionEventsFusionPhaseStatus")] [JsonSerializable(typeof(GitHub.Copilot.FusionPhaseUsage), TypeInfoPropertyName = "SessionEventsFusionPhaseUsage")] [JsonSerializable(typeof(GitHub.Copilot.FusionProjectionMode), TypeInfoPropertyName = "SessionEventsFusionProjectionMode")] @@ -36181,6 +36510,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(DiscoveredExtensions))] [JsonSerializable(typeof(DiscoveredExtensionsDisableRequest))] [JsonSerializable(typeof(DiscoveredExtensionsEnableRequest))] +[JsonSerializable(typeof(DiscoveredHook))] [JsonSerializable(typeof(DiscoveredMcpServer))] [JsonSerializable(typeof(EnqueueCommandParams))] [JsonSerializable(typeof(EnqueueCommandResult))] @@ -36265,6 +36595,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(HistorySummarizeForHandoffResult))] [JsonSerializable(typeof(HistoryTruncateRequest))] [JsonSerializable(typeof(HistoryTruncateResult))] +[JsonSerializable(typeof(HooksDiscoverRequest))] +[JsonSerializable(typeof(HooksDiscoverResult))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] @@ -36812,6 +37144,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsLoadDeferredRepoHooksRequest))] [JsonSerializable(typeof(SessionsOpenProgress))] [JsonSerializable(typeof(SessionsPruneOldRequest))] +[JsonSerializable(typeof(SessionsReadPersistedEventsRequest))] [JsonSerializable(typeof(SessionsRegisterExtensionToolsOnSessionOptions))] [JsonSerializable(typeof(SessionsReleaseLockRequest))] [JsonSerializable(typeof(SessionsReleaseLockResult))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 170e83027a..f832b95d85 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -26,6 +26,7 @@ namespace GitHub.Copilot; IgnoreUnrecognizedTypeDiscriminators = true)] [JsonDerivedType(typeof(AbortEvent), "abort")] [JsonDerivedType(typeof(AgentInterruptedEvent), "agent.interrupted")] +[JsonDerivedType(typeof(AssistantFusionPhaseActivityEvent), "assistant.fusion_phase_activity")] [JsonDerivedType(typeof(AssistantFusionPhaseCompletedEvent), "assistant.fusion_phase_completed")] [JsonDerivedType(typeof(AssistantFusionPhaseFailedEvent), "assistant.fusion_phase_failed")] [JsonDerivedType(typeof(AssistantFusionPhaseStartedEvent), "assistant.fusion_phase_started")] @@ -94,6 +95,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionCanvasUnavailableEvent), "session.canvas.unavailable")] [JsonDerivedType(typeof(SessionCompactionCompleteEvent), "session.compaction_complete")] [JsonDerivedType(typeof(SessionCompactionStartEvent), "session.compaction_start")] +[JsonDerivedType(typeof(SessionCompletionReceiptEvent), "session.completion_receipt")] [JsonDerivedType(typeof(SessionContextChangedEvent), "session.context_changed")] [JsonDerivedType(typeof(SessionContextClearedEvent), "session.context_cleared")] [JsonDerivedType(typeof(SessionCustomAgentsUpdatedEvent), "session.custom_agents_updated")] @@ -601,6 +603,20 @@ public sealed partial class SessionTaskCompleteEvent : SessionEvent public required SessionTaskCompleteData Data { get; set; } } +/// Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +/// Represents the session.completion_receipt event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionCompletionReceiptEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.completion_receipt"; + + /// The session.completion_receipt event payload. + [JsonPropertyName("data")] + public required SessionCompletionReceiptData Data { get; set; } +} + /// Experimental transient signal that HydraFusion routing has started for an eligible turn. /// Represents the session.fusion_route_started event. [Experimental(Diagnostics.Experimental)] @@ -749,6 +765,20 @@ public sealed partial class AssistantFusionPhaseStartedEvent : SessionEvent public required AssistantFusionPhaseStartedData Data { get; set; } } +/// Experimental content-safe activity signal for a running HydraFusion phase. +/// Represents the assistant.fusion_phase_activity event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseActivityEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "assistant.fusion_phase_activity"; + + /// The assistant.fusion_phase_activity event payload. + [JsonPropertyName("data")] + public required AssistantFusionPhaseActivityData Data { get; set; } +} + /// Experimental durable HydraFusion phase output and lossless replay checkpoint. /// Represents the assistant.fusion_phase_completed event. [Experimental(Diagnostics.Experimental)] @@ -2893,6 +2923,44 @@ public sealed partial class SessionTaskCompleteData public string? Summary { get; set; } } +/// Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionCompletionReceiptData +{ + /// One-based accepted completion receipt ordinal in the durable session history. + [JsonPropertyName("attempt")] + public required long Attempt { get; set; } + + /// Inclusive durable event range summarized by this receipt. + [JsonPropertyName("eventRange")] + public required CompletionReceiptEventRange EventRange { get; set; } + + /// Number of failed structured tool completions in the covered range. + [JsonPropertyName("failedToolCount")] + public required long FailedToolCount { get; set; } + + /// Final structured tool completion in the covered range, when one exists. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("finalTool")] + public CompletionReceiptFinalTool? FinalTool { get; set; } + + /// Version of the completion receipt payload. + [JsonPropertyName("schemaVersion")] + public required long SchemaVersion { get; set; } + + /// Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + [JsonPropertyName("sourceEventId")] + public required string SourceEventId { get; set; } + + /// Runtime reason the completion decision was accepted. + [JsonPropertyName("stopReason")] + public required CompletionReceiptStopReason StopReason { get; set; } + + /// Number of successful structured tool completions in the covered range. + [JsonPropertyName("successfulToolCount")] + public required long SuccessfulToolCount { get; set; } +} + /// Experimental transient signal that HydraFusion routing has started for an eligible turn. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionFusionRouteStartedData @@ -2985,6 +3053,12 @@ public sealed partial class SessionFusionResolvedData [JsonPropertyName("pattern")] public required FusionPattern Pattern { get; set; } + /// Presentation-neutral phase plan for clients that render workflow progress. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phasePlan")] + public FusionPhasePlanStep[]? PhasePlan { get; set; } + /// Version of the validated execution-plan format. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("planVersion")] @@ -3156,6 +3230,11 @@ public sealed partial class UserMessageData [JsonPropertyName("isAutopilotContinuation")] public bool? IsAutopilotContinuation { get; set; } + /// Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("messageId")] + public string? MessageId { get; set; } + /// Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("nativeDocumentPathFallbackPaths")] @@ -3337,6 +3416,49 @@ public sealed partial class AssistantFusionPhaseStartedData public required string Role { get; set; } } +/// Experimental content-safe activity signal for a running HydraFusion phase. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseActivityData +{ + /// Kind of real activity observed. + [JsonPropertyName("activity")] + public required FusionPhaseActivityKind Activity { get; set; } + + /// Conversation scope in which the phase executes. + [JsonPropertyName("conversationScope")] + public required FusionConversationScope ConversationScope { get; set; } + + /// Identifier of the HydraFusion turn containing the phase. + [JsonPropertyName("fusionId")] + public required string FusionId { get; set; } + + /// HydraFusion orchestration pattern containing the phase. + [JsonPropertyName("pattern")] + public required FusionPattern Pattern { get; set; } + + /// Stable identifier for the concrete phase. + [JsonPropertyName("phaseId")] + public required string PhaseId { get; set; } + + /// Kind of phase currently executing. + [JsonPropertyName("phaseKind")] + public required FusionPhaseKind PhaseKind { get; set; } + + /// Semantic role assigned to the phase. + [JsonPropertyName("role")] + public required string Role { get; set; } + + /// Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + /// Cumulative private response bytes observed for this model call. The event never includes response text. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("totalResponseSizeBytes")] + public long? TotalResponseSizeBytes { get; set; } +} + /// Experimental durable HydraFusion phase output and lossless replay checkpoint. [Experimental(Diagnostics.Experimental)] public sealed partial class AssistantFusionPhaseCompletedData @@ -4655,6 +4777,11 @@ public sealed partial class SubagentCompletedData [JsonPropertyName("model")] public string? Model { get; set; } + /// Why an explicit task-call model did not become the effective model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelOverrideReason")] + public string? ModelOverrideReason { get; set; } + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -4721,6 +4848,11 @@ public sealed partial class SubagentFailedData [JsonPropertyName("model")] public string? Model { get; set; } + /// Why an explicit task-call model did not become the effective model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelOverrideReason")] + public string? ModelOverrideReason { get; set; } + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -4900,6 +5032,11 @@ public sealed partial class SystemNotificationData /// Permission request notification requiring client approval with request details. public sealed partial class PermissionRequestedData { + /// Agent mode captured from the owning turn when permission evaluation began. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentMode")] + public SessionMode? AgentMode { get; set; } + /// Details of the permission being requested. [JsonPropertyName("permissionRequest")] public required PermissionRequest PermissionRequest { get; set; } @@ -6238,6 +6375,42 @@ public sealed partial class CompactionCompleteCompactionTokensUsed public long? OutputTokens { get; set; } } +/// Inclusive durable event range summarized by a completion receipt. +/// Nested data type for CompletionReceiptEventRange. +public sealed partial class CompletionReceiptEventRange +{ + /// Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + [JsonPropertyName("endEventId")] + public required string EndEventId { get; set; } + + /// Identifier of the user message that starts the covered exchange. + [JsonPropertyName("startEventId")] + public required string StartEventId { get; set; } +} + +/// Final structured tool completion in the covered event range. +/// Nested data type for CompletionReceiptFinalTool. +public sealed partial class CompletionReceiptFinalTool +{ + /// Process exit code from a structured shell result, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("exitCode")] + public long? ExitCode { get; set; } + + /// Structured success or failure status from the tool completion event. + [JsonPropertyName("status")] + public required CompletionReceiptToolStatus Status { get; set; } + + /// Unique identifier of the completed tool call. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } + + /// Tool name from the matching tool execution start event, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} + /// Durable server recommendation for subsequent HydraFusion turns. /// Nested data type for FusionFollowUpRecommendation. [Experimental(Diagnostics.Experimental)] @@ -6252,6 +6425,28 @@ public sealed partial class FusionFollowUpRecommendation public required FusionFollowUpAction UserTurn { get; set; } } +/// Presentation-neutral phase planned for a HydraFusion turn. +/// Nested data type for FusionPhasePlanStep. +[Experimental(Diagnostics.Experimental)] +public sealed partial class FusionPhasePlanStep +{ + /// Whether the phase executes only when an earlier phase requests it. + [JsonPropertyName("conditional")] + public required bool Conditional { get; set; } + + /// Kind of phase that may execute. + [JsonPropertyName("kind")] + public required FusionPhaseKind Kind { get; set; } + + /// Semantic role assigned to the phase. + [JsonPropertyName("role")] + public required string Role { get; set; } + + /// Conversation scope in which the phase executes. + [JsonPropertyName("scope")] + public required FusionConversationScope Scope { get; set; } +} + /// Validated HydraFusion routing capability scores. /// Nested data type for FusionScores. [Experimental(Diagnostics.Experimental)] @@ -10170,7 +10365,7 @@ public sealed partial class SkillsLoadedSkill public required bool UserInvocable { get; set; } } -/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. +/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. /// Nested data type for CustomAgentsUpdatedAgent. public sealed partial class CustomAgentsUpdatedAgent { @@ -10191,6 +10386,16 @@ public sealed partial class CustomAgentsUpdatedAgent [JsonPropertyName("model")] public string? Model { get; set; } + /// Whether authored models are preferences or required constraints. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } + + /// Authored model ids in priority order, if configured. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("models")] + public string[]? Models { get; set; } + /// Internal name of the agent. [JsonPropertyName("name")] public required string Name { get; set; } @@ -11469,6 +11674,140 @@ public override void Write(Utf8JsonWriter writer, TaskCompletionOutcome value, J } } +/// Structured terminal status from a tool completion event. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CompletionReceiptToolStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CompletionReceiptToolStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The tool completed successfully. + public static CompletionReceiptToolStatus Success { get; } = new("success"); + + /// The tool failed without a more specific structured status. + public static CompletionReceiptToolStatus Failure { get; } = new("failure"); + + /// The tool exceeded its time budget. + public static CompletionReceiptToolStatus Timeout { get; } = new("timeout"); + + /// The user rejected the tool call. + public static CompletionReceiptToolStatus Rejected { get; } = new("rejected"); + + /// The permissions service denied the tool call. + public static CompletionReceiptToolStatus Denied { get; } = new("denied"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CompletionReceiptToolStatus left, CompletionReceiptToolStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CompletionReceiptToolStatus left, CompletionReceiptToolStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is CompletionReceiptToolStatus other && Equals(other); + + /// + public bool Equals(CompletionReceiptToolStatus 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 CompletionReceiptToolStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, CompletionReceiptToolStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CompletionReceiptToolStatus)); + } + } +} + +/// Runtime reason the completion decision was accepted. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CompletionReceiptStopReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CompletionReceiptStopReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The model reached a natural terminal response. + public static CompletionReceiptStopReason Natural { get; } = new("natural"); + + /// A terminal tool ended the interaction. + public static CompletionReceiptStopReason TerminalTool { get; } = new("terminal_tool"); + + /// The configured agentStop continuation limit was reached. + public static CompletionReceiptStopReason AgentStopBlockLimit { get; } = new("agent_stop_block_limit"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CompletionReceiptStopReason left, CompletionReceiptStopReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CompletionReceiptStopReason left, CompletionReceiptStopReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is CompletionReceiptStopReason other && Equals(other); + + /// + public bool Equals(CompletionReceiptStopReason 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 CompletionReceiptStopReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, CompletionReceiptStopReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CompletionReceiptStopReason)); + } + } +} + /// Kind of turn for which HydraFusion routing is running. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -11658,6 +11997,145 @@ public override void Write(Utf8JsonWriter writer, FusionPattern value, JsonSeria } } +/// HydraFusion phase kind. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionPhaseKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionPhaseKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Primary solver phase. + public static FusionPhaseKind Primary { get; } = new("primary"); + + /// Read-only cascade judge phase. + public static FusionPhaseKind Judge { get; } = new("judge"); + + /// Cascade repair phase. + public static FusionPhaseKind Repair { get; } = new("repair"); + + /// Initial critique-pattern draft phase. + public static FusionPhaseKind Draft { get; } = new("draft"); + + /// Read-only critique phase. + public static FusionPhaseKind Critic { get; } = new("critic"); + + /// Critique-pattern revision phase. + public static FusionPhaseKind Revision { get; } = new("revision"); + + /// Follow-up phase continuing from the resolved model. + public static FusionPhaseKind FollowUp { get; } = new("follow_up"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionPhaseKind left, FusionPhaseKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionPhaseKind left, FusionPhaseKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionPhaseKind other && Equals(other); + + /// + public bool Equals(FusionPhaseKind 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 FusionPhaseKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionPhaseKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseKind)); + } + } +} + +/// Conversation scope in which a HydraFusion phase executes. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionConversationScope : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionConversationScope(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Canonical root conversation history. + public static FusionConversationScope Root { get; } = new("root"); + + /// Isolated read-only review history that does not enter the root conversation. + public static FusionConversationScope Review { get; } = new("review"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionConversationScope left, FusionConversationScope right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionConversationScope left, FusionConversationScope right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionConversationScope other && Equals(other); + + /// + public bool Equals(FusionConversationScope 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 FusionConversationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionConversationScope value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionConversationScope)); + } + } +} + /// The agent mode that was active when this message was sent. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -12103,120 +12581,46 @@ public override void Write(Utf8JsonWriter writer, ModelCallFailureTransport valu } } -/// Conversation scope in which a HydraFusion phase executes. +/// Content-safe activity observed while a HydraFusion phase is running. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FusionConversationScope : IEquatable +public readonly struct FusionPhaseActivityKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FusionConversationScope(string value) + public FusionPhaseActivityKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Canonical root conversation history. - public static FusionConversationScope Root { get; } = new("root"); + /// The provider produced additional private output bytes. + public static FusionPhaseActivityKind ModelOutput { get; } = new("model_output"); - /// Isolated read-only review history that does not enter the root conversation. - public static FusionConversationScope Review { get; } = new("review"); + /// A tool began executing inside the phase. + public static FusionPhaseActivityKind ToolStarted { get; } = new("tool_started"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FusionConversationScope left, FusionConversationScope right) => left.Equals(right); + /// A tool finished executing inside the phase. + public static FusionPhaseActivityKind ToolCompleted { get; } = new("tool_completed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FusionConversationScope left, FusionConversationScope right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionPhaseActivityKind left, FusionPhaseActivityKind right) => left.Equals(right); - /// - public override bool Equals(object? obj) => obj is FusionConversationScope other && Equals(other); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionPhaseActivityKind left, FusionPhaseActivityKind right) => !(left == right); /// - public bool Equals(FusionConversationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public override bool Equals(object? obj) => obj is FusionPhaseActivityKind other && Equals(other); /// - 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 FusionConversationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, FusionConversationScope value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionConversationScope)); - } - } -} - -/// HydraFusion phase kind. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct FusionPhaseKind : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public FusionPhaseKind(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Primary solver phase. - public static FusionPhaseKind Primary { get; } = new("primary"); - - /// Read-only cascade judge phase. - public static FusionPhaseKind Judge { get; } = new("judge"); - - /// Cascade repair phase. - public static FusionPhaseKind Repair { get; } = new("repair"); - - /// Initial critique-pattern draft phase. - public static FusionPhaseKind Draft { get; } = new("draft"); - - /// Read-only critique phase. - public static FusionPhaseKind Critic { get; } = new("critic"); - - /// Critique-pattern revision phase. - public static FusionPhaseKind Revision { get; } = new("revision"); - - /// Follow-up phase continuing from the resolved model. - public static FusionPhaseKind FollowUp { get; } = new("follow_up"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FusionPhaseKind left, FusionPhaseKind right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FusionPhaseKind left, FusionPhaseKind right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is FusionPhaseKind other && Equals(other); - - /// - public bool Equals(FusionPhaseKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(FusionPhaseActivityKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -12224,20 +12628,20 @@ public FusionPhaseKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FusionPhaseKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override FusionPhaseActivityKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FusionPhaseKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, FusionPhaseActivityKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseActivityKind)); } } } @@ -15251,6 +15655,67 @@ public override void Write(Utf8JsonWriter writer, SkillSource value, JsonSeriali } } +/// Whether configured models are advisory preferences or required constraints. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AgentModelPolicy : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AgentModelPolicy(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Treat the authored models as advisory preferences that callers may override. + public static AgentModelPolicy Preferred { get; } = new("preferred"); + + /// Require subagent execution to use one of the authored models. + public static AgentModelPolicy Required { get; } = new("required"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentModelPolicy left, AgentModelPolicy right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentModelPolicy left, AgentModelPolicy right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AgentModelPolicy other && Equals(other); + + /// + public bool Equals(AgentModelPolicy 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 AgentModelPolicy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AgentModelPolicy value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentModelPolicy)); + } + } +} + /// Configuration source: user, workspace, plugin, or builtin. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -15604,6 +16069,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(AbortEvent))] [JsonSerializable(typeof(AgentInterruptedData))] [JsonSerializable(typeof(AgentInterruptedEvent))] +[JsonSerializable(typeof(AssistantFusionPhaseActivityData))] +[JsonSerializable(typeof(AssistantFusionPhaseActivityEvent))] [JsonSerializable(typeof(AssistantFusionPhaseCompletedData))] [JsonSerializable(typeof(AssistantFusionPhaseCompletedEvent))] [JsonSerializable(typeof(AssistantFusionPhaseFailedData))] @@ -15698,6 +16165,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsed))] [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsedCopilotUsage))] [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail))] +[JsonSerializable(typeof(CompletionReceiptEventRange))] +[JsonSerializable(typeof(CompletionReceiptFinalTool))] [JsonSerializable(typeof(CustomAgentsUpdatedAgent))] [JsonSerializable(typeof(ElicitationCompletedData))] [JsonSerializable(typeof(ElicitationCompletedEvent))] @@ -15724,6 +16193,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(FactoryRunUpdatedEvent))] [JsonSerializable(typeof(FusionAttribution))] [JsonSerializable(typeof(FusionFollowUpRecommendation))] +[JsonSerializable(typeof(FusionPhasePlanStep))] [JsonSerializable(typeof(FusionPhaseUsage))] [JsonSerializable(typeof(FusionScores))] [JsonSerializable(typeof(FusionStagedTerminal))] @@ -15851,6 +16321,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionCompactionCompleteEvent))] [JsonSerializable(typeof(SessionCompactionStartData))] [JsonSerializable(typeof(SessionCompactionStartEvent))] +[JsonSerializable(typeof(SessionCompletionReceiptData))] +[JsonSerializable(typeof(SessionCompletionReceiptEvent))] [JsonSerializable(typeof(SessionContextChangedData))] [JsonSerializable(typeof(SessionContextChangedEvent))] [JsonSerializable(typeof(SessionContextClearedData))] diff --git a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs index 0d2942d4bf..ec923b31a1 100644 --- a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs +++ b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs @@ -33,7 +33,7 @@ public async Task Should_List_And_Toggle_Session_Skills() { var skillName = $"session-rpc-skill-{Guid.NewGuid():N}"; var skillsDir = CreateSkillDirectory(skillName, "Session skill controlled by RPC."); - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir], DisabledSkills = [skillName], @@ -56,7 +56,7 @@ public async Task Should_Ensure_Skills_Are_Loaded_And_List_Invoked_Skills() { var skillName = $"ensure-rpc-skill-{Guid.NewGuid():N}"; var skillsDir = CreateSkillDirectory(skillName, "Skill loaded explicitly by RPC."); - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir], }); @@ -79,7 +79,7 @@ public async Task Should_Reload_Session_Skills() Directory.CreateDirectory(skillsDir); var skillName = $"reload-rpc-skill-{Guid.NewGuid():N}"; - var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir] }); + await using var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir] }); var before = await session.Rpc.Skills.ListAsync(); Assert.DoesNotContain(before.Skills, skill => string.Equals(skill.Name, skillName, StringComparison.Ordinal)); @@ -95,7 +95,7 @@ public async Task Should_Reload_Session_Skills() public async Task Should_List_Mcp_Servers_With_Configured_Server() { const string serverName = "rpc-list-mcp-server"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); @@ -111,7 +111,7 @@ public async Task Should_List_Mcp_Servers_With_Configured_Server() public async Task Should_Set_Mcp_Env_Value_Mode_And_Remove_GitHub_Server() { const string serverName = "github"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); @@ -137,7 +137,7 @@ public async Task Should_Set_Mcp_Env_Value_Mode_And_Remove_GitHub_Server() public async Task Should_Report_Mcp_Sampling_Failure_And_Cancel_Missing_Sampling() { const string serverName = "rpc-sampling-server"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); @@ -172,7 +172,7 @@ public async Task Should_Report_Mcp_Sampling_Failure_And_Cancel_Missing_Sampling [Fact] public async Task Should_List_Plugins() { - var session = await CreateSessionAsync(); + await using var session = await CreateSessionAsync(); var result = await session.Rpc.Plugins.ListAsync(); @@ -314,7 +314,7 @@ public async Task Should_Report_Error_When_Mcp_App_Resource_Is_Not_Available() [Fact] public async Task Should_Report_Error_When_Mcp_Host_Is_Not_Initialized() { - var session = await CreateSessionAsync(); + await using var session = await CreateSessionAsync(); await AssertFailureAsync( () => session.Rpc.Mcp.EnableAsync("missing-server"), @@ -333,7 +333,7 @@ await AssertFailureAsync( [Fact] public async Task Should_Report_Error_When_Mcp_Oauth_Server_Is_Not_Configured() { - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers("configured-stdio-server"), }); @@ -348,7 +348,7 @@ await AssertFailureAsync( public async Task Should_Report_Error_When_Mcp_Oauth_Server_Is_Not_Remote() { const string serverName = "configured-stdio-server"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 1b2458590d..70f3bcbbb4 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -170,7 +170,7 @@ type AgentGetCurrentResult struct { Agent *AgentInfo `json:"agent,omitempty"` } -// Agent metadata, including identifiers, display details, source, tools, model, MCP +// Agent metadata, including identifiers, display details, source, tools, model, models, MCP // servers, skills, and file path. // Experimental: AgentInfo is part of an experimental API and may change or be removed. type AgentInfo struct { @@ -189,6 +189,11 @@ type AgentInfo struct { // Authored preferred model id for this agent. Runtime model selection may choose a // different model; omitted means no authored preference. Model *string `json:"model,omitempty"` + // Whether authored models are preferences or required constraints. + ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` + // Authored preferred model ids for this agent, in priority order. Runtime model selection + // chooses the first available model; omitted means no authored preference. + Models []string `json:"models,omitzero"` // Name of the agent. Use `id` as the stable selection identifier. Name string `json:"name"` // Absolute local file path of the agent definition. Only set for file-based agents loaded @@ -2582,6 +2587,36 @@ type DiscoveredExtensionsEnableRequest struct { IDs []string `json:"ids"` } +// One server-discovered hook action from user, repository, plugin, or managed-policy +// configuration. +// Experimental: DiscoveredHook is part of an experimental API and may change or be removed. +type DiscoveredHook struct { + // Durable content hash used by hook enablement. Identical actions may intentionally share + // this key. Omitted when changing the user's disabled-hooks setting cannot change the + // action's current server-discovered state, including managed-policy hooks, session-start + // prompt actions, actions suppressed by disable-all settings, and projectless plugin + // actions that require project-directory expansion. + DisableKey *string `json:"disableKey,omitempty"` + // Whether this action is enabled under the server-side discovery settings. Concrete + // sessions may differ because they can add session-specific directories, plugins, or trust. + // False when its disable key is present in the user's disabled-hooks setting or disable-all + // settings suppress the action. + Enabled bool `json:"enabled"` + // Hook event that invokes this action. + HookType HookType `json:"hookType"` + // Deterministic identifier for this server-discovered action row. It remains stable while + // the project, origin, source, event, action content, and duplicate ordinal are unchanged. + // This is row identity, not the key persisted in disabledHooks. + ID string `json:"id"` + // Configuration tier that contributed this hook action. + Origin HookOrigin `json:"origin"` + // Input project path for which this server-side action was resolved. Set on every row + // returned for project-scoped discovery, including repeated user and policy actions. + ProjectPath *string `json:"projectPath,omitempty"` + // Human-readable source label, such as a hook file path, settings source, or plugin name. + Source *string `json:"source,omitempty"` +} + // MCP server discovered by `mcp.discover`, with config source, optional plugin source, // transport type, and enabled state. // Experimental: DiscoveredMCPServer is part of an experimental API and may change or be @@ -4229,8 +4264,6 @@ type HistoryTruncateResult struct { // removed. // Internal: HookInvokeRequest is an internal SDK API and is not part of the public surface. type HookInvokeRequest struct { - // Internal: HookType is part of the SDK's internal API surface and is not intended for - // external use. HookType HookType `json:"hookType"` Input any `json:"input"` SessionID string `json:"sessionId"` @@ -4244,6 +4277,40 @@ type HookInvokeResponse struct { Output any `json:"output,omitempty"` } +// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +// Experimental: HooksDiscoverRequest is part of an experimental API and may change or be +// removed. +type HooksDiscoverRequest struct { + // When true, omit host-owned user and plugin hook rows and their diagnostics. + // Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks + // still contribute to each remaining row's effective enabled state. This filters sources + // rather than simulating a host with no settings. + ExcludeHostHooks *bool `json:"excludeHostHooks,omitempty"` + // Optional project directory paths whose trusted repository and project-expanded plugin + // hooks should be discovered. When omitted or empty, user, managed-policy, and globally + // enabled installed or explicit plugin hooks are returned without project expansion. + ProjectPaths []string `json:"projectPaths,omitzero"` +} + +// Server-discovered hook actions and partial-load diagnostics from user, repository, +// plugin, and managed-policy sources. Concrete sessions may include additional +// session-specific hook sources. +// Experimental: HooksDiscoverResult is part of an experimental API and may change or be +// removed. +type HooksDiscoverResult struct { + // Errors for hook sources or actions that could not be loaded, making the result partially + // incomplete. Other valid actions are still returned. Project-resolution and + // repository-settings errors are prefixed with their project path. + Errors []string `json:"errors"` + // All discovered hook actions. Byte-identical actions remain separate rows even when they + // share a disable key. + Hooks []DiscoveredHook `json:"hooks"` + // Non-fatal source-loading warnings. Discovery remains complete for the affected source, + // although the source had a recoverable issue. Repository-settings warnings are prefixed + // with their project path when attribution is available. + Warnings []string `json:"warnings"` +} + // Installed plugin record from global state, with marketplace, version, install time, // enabled state, cache path, and source. // Experimental: InstalledPlugin is part of an experimental API and may change or be removed. @@ -7244,8 +7311,8 @@ type ModelSwitchToRequest struct { RequireAvailable *bool `json:"requireAvailable,omitempty"` // When true, evaluate context-window compaction policy before applying the switch. RunCompactionPreflight *bool `json:"runCompactionPreflight,omitempty"` - // Origin to record on the effective `session.model_change` event. Defaults to `sdk` when - // omitted. + // Origin to record on the effective `session.model_change` event for trusted in-process + // calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. Source *ModelChangeSource `json:"source,omitempty"` // Output verbosity level to request for supported models Verbosity *Verbosity `json:"verbosity,omitempty"` @@ -9825,6 +9892,9 @@ type QueuePendingItems struct { ID string `json:"id"` // Whether this item is a queued user message or a queued slash command / model change Kind QueuePendingItemsKind `json:"kind"` + // Stable identity of the queued user message. Present for message rows and absent for slash + // commands and model changes. + MessageID *string `json:"messageId,omitempty"` } // Snapshot of the session's pending queued items and immediate-steering messages. @@ -10336,11 +10406,14 @@ type SandboxConfigUserPolicyNetwork struct { AllowLocalNetwork *bool `json:"allowLocalNetwork,omitempty"` // Whether outbound network traffic is allowed at all. AllowOutbound *bool `json:"allowOutbound,omitempty"` - // HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and - // cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. - // Credentials go in the separate `username`/`password` fields. A credential-free http:// - // loopback proxy URL is routed through the localhost proxy automatically; an https:// or - // authenticated loopback URL is used as-is. + // HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, + // requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is + // accepted and routed through the IPv4 gateway), and does not support proxy credentials. + // macOS relies on applications honoring proxy environment variables. Windows also + // configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's + // networking stack. Configure supported credentials in the separate `username` and + // `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, + // while an https:// or authenticated loopback URL uses the URL form. Proxy *SandboxConfigUserPolicyNetworkProxy `json:"proxy,omitempty"` } @@ -10356,12 +10429,12 @@ type SandboxConfigUserPolicyNetworkProxy struct { // settings.json); the field is masked in the dialog and redacted by /settings show. Password *string `json:"password,omitempty"` // Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the - // scheme's standard port when omitted. Credentials must not be embedded here — a - // `user:pass@` authority is rejected; put them in the separate `username`/`password` - // fields. A credential-free http:// loopback URL is routed through the localhost proxy - // automatically; loopback covers localhost and any *.localhost subdomain, the whole - // 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or - // one with a username/password set, is used as-is. + // scheme's standard port when omitted; an explicit port must be between 1 and 65535. + // Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in + // the separate `username`/`password` fields. A credential-free http:// loopback proxy URL + // is routed through the localhost proxy automatically; loopback covers localhost and any + // *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback + // (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. URL string `json:"url"` // Optional username for proxy authentication. Combined with the URL (and `password`) into // `user:pass@host` when the sandboxed process routes through the proxy. @@ -12902,6 +12975,22 @@ type SessionsPruneOldRequest struct { OlderThanDays int64 `json:"olderThanDays"` } +// Pagination options for reading an inactive or active local session's persisted event +// journal. +// Experimental: SessionsReadPersistedEventsRequest is part of an experimental API and may +// change or be removed. +type SessionsReadPersistedEventsRequest struct { + // Opaque cursor returned by a previous persisted-event read. Omit on the first call. + Cursor *string `json:"cursor,omitempty"` + // Direction to page through persisted history. Forward starts at the beginning; backward + // starts with the newest events. Events in each page remain chronological. + Direction *EventsReadDirection `json:"direction,omitempty"` + // Maximum number of events to return in this batch (1–1000, default 200). + Max *int64 `json:"max,omitempty"` + // Session ID whose persisted event journal should be read. + SessionID string `json:"sessionId"` +} + // Optional registration options. // Experimental: SessionsRegisterExtensionToolsOnSessionOptions is part of an experimental // API and may change or be removed. @@ -13902,6 +13991,8 @@ type SubagentSettingsEntry struct { EffortLevel *string `json:"effortLevel,omitempty"` // Model override for matching subagents Model *string `json:"model,omitempty"` + // Whether the configured model strategy is preferred or required + ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` } // Task completion notification with summary from the agent @@ -15713,6 +15804,18 @@ const ( AgentInfoSourceUser AgentInfoSource = "user" ) +// Whether configured models are advisory preferences or required constraints +// Experimental: AgentModelPolicy is part of an experimental API and may change or be +// removed. +type AgentModelPolicy string + +const ( + // Treat the authored models as advisory preferences that callers may override. + AgentModelPolicyPreferred AgentModelPolicy = "preferred" + // Require subagent execution to use one of the authored models. + AgentModelPolicyRequired AgentModelPolicy = "required" +) + // Kind of attention required when status === "attention". Meaningful only when status === // "attention". // Experimental: AgentRegistryLiveTargetEntryAttentionKind is part of an experimental API @@ -16784,7 +16887,24 @@ const ( HMACAuthInfoHostHTTPSGitHubCom HMACAuthInfoHost = "https://github.com" ) -// Hook event name dispatched through the SDK callback transport. +// Configuration tier that contributed a discovered hook action. +// Experimental: HookOrigin is part of an experimental API and may change or be removed. +type HookOrigin string + +const ( + // Hook provided by an enabled installed or explicit plugin. Projectless rows omit + // projectPath and do not expand a project directory. + HookOriginPlugin HookOrigin = "plugin" + // Hook enforced by centrally managed policy. + HookOriginPolicy HookOrigin = "policy" + // Hook loaded from repository settings or the repository hook directory. + HookOriginRepository HookOrigin = "repository" + // Hook loaded from user settings or the user's hook directory. + HookOriginUser HookOrigin = "user" +) + +// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally +// support callback-only events. // Experimental: HookType is part of an experimental API and may change or be removed. type HookType string @@ -19348,6 +19468,32 @@ func (a *ServerExtensionsAPI) Enable(ctx context.Context, params *DiscoveredExte return &result, nil } +// Experimental: ServerHooksAPI contains experimental APIs that may change or be removed. +type ServerHooksAPI serverAPI + +// Discovers hook actions enabled under server-side discovery settings from user, +// repository, plugin, and managed-policy sources. +// +// RPC method: hooks.discover. +// +// Parameters: Optional project paths and host-exclusion behavior for server-scoped hook +// discovery. +// +// Returns: Server-discovered hook actions and partial-load diagnostics from user, +// repository, plugin, and managed-policy sources. Concrete sessions may include additional +// session-specific hook sources. +func (a *ServerHooksAPI) Discover(ctx context.Context, params *HooksDiscoverRequest) (*HooksDiscoverResult, error) { + raw, err := a.client.Request(ctx, "hooks.discover", params) + if err != nil { + return nil, err + } + var result HooksDiscoverResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ServerInstructionsAPI contains experimental APIs that may change or be // removed. type ServerInstructionsAPI serverAPI @@ -20332,6 +20478,31 @@ func (a *ServerSessionsAPI) PruneOld(ctx context.Context, params *SessionsPruneO return &result, nil } +// ReadPersistedEvents reads a page of durable events directly from a local session's +// persisted journal without creating, resuming, or activating the session. The initial +// backward read uses a bounded tail scan for fast first paint; cursor continuations +// preserve the session event-log paging semantics. Persisted events may omit payloads that +// are reconstructed only for an active session. +// +// RPC method: sessions.readPersistedEvents. +// +// Parameters: Pagination options for reading an inactive or active local session's +// persisted event journal. +// +// Returns: Batch of session events returned by a read, with cursor and continuation +// metadata. +func (a *ServerSessionsAPI) ReadPersistedEvents(ctx context.Context, params *SessionsReadPersistedEventsRequest) (*EventsReadResult, error) { + raw, err := a.client.Request(ctx, "sessions.readPersistedEvents", params) + if err != nil { + return nil, err + } + var result EventsReadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // ReleaseLock releases the in-use lock held by this process for a session. // // RPC method: sessions.releaseLock. @@ -20705,6 +20876,7 @@ type ServerRPC struct { Catalog *ServerCatalogAPI Commands *ServerCommandsAPI Extensions *ServerExtensionsAPI + Hooks *ServerHooksAPI Instructions *ServerInstructionsAPI LlmInference *ServerLlmInferenceAPI ManagedSettings *ServerManagedSettingsAPI @@ -20769,6 +20941,7 @@ func NewServerRPC(client *jsonrpc2.Client) *ServerRPC { r.Catalog = (*ServerCatalogAPI)(&r.common) r.Commands = (*ServerCommandsAPI)(&r.common) r.Extensions = (*ServerExtensionsAPI)(&r.common) + r.Hooks = (*ServerHooksAPI)(&r.common) r.Instructions = (*ServerInstructionsAPI)(&r.common) r.LlmInference = (*ServerLlmInferenceAPI)(&r.common) r.ManagedSettings = (*ServerManagedSettingsAPI)(&r.common) diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index 05e372402b..bbe9fbc74b 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -47,6 +47,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeAssistantFusionPhaseActivity: + var d AssistantFusionPhaseActivityData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeAssistantFusionPhaseCompleted: var d AssistantFusionPhaseCompletedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -443,6 +449,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionCompletionReceipt: + var d SessionCompletionReceiptData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionContextChanged: var d SessionContextChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -864,6 +876,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { Delivery *UserMessageDelivery `json:"delivery,omitempty"` InteractionID *string `json:"interactionId,omitempty"` IsAutopilotContinuation *bool `json:"isAutopilotContinuation,omitempty"` + MessageID *string `json:"messageId,omitempty"` NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitzero"` ParentAgentTaskID *string `json:"parentAgentTaskId,omitempty"` Source *string `json:"source,omitempty"` @@ -890,6 +903,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { r.Delivery = raw.Delivery r.InteractionID = raw.InteractionID r.IsAutopilotContinuation = raw.IsAutopilotContinuation + r.MessageID = raw.MessageID r.NativeDocumentPathFallbackPaths = raw.NativeDocumentPathFallbackPaths r.ParentAgentTaskID = raw.ParentAgentTaskID r.Source = raw.Source @@ -2101,6 +2115,7 @@ func (r PermissionPromptRequestWrite) MarshalJSON() ([]byte, error) { func (r *PermissionRequestedData) UnmarshalJSON(data []byte) error { type rawPermissionRequestedData struct { + AgentMode *SessionMode `json:"agentMode,omitempty"` PermissionRequest json.RawMessage `json:"permissionRequest"` PromptRequest json.RawMessage `json:"promptRequest,omitempty"` RequestID string `json:"requestId"` @@ -2111,6 +2126,7 @@ func (r *PermissionRequestedData) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &raw); err != nil { return err } + r.AgentMode = raw.AgentMode if raw.PermissionRequest != nil { value, err := unmarshalPermissionRequest(raw.PermissionRequest) if err != nil { diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 0f93b52f18..a7d9d43af3 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -55,6 +55,9 @@ type SessionEventType string const ( SessionEventTypeAbort SessionEventType = "abort" SessionEventTypeAgentInterrupted SessionEventType = "agent.interrupted" + // Experimental: SessionEventTypeAssistantFusionPhaseActivity identifies an experimental + // event that may change or be removed. + SessionEventTypeAssistantFusionPhaseActivity SessionEventType = "assistant.fusion_phase_activity" // Experimental: SessionEventTypeAssistantFusionPhaseCompleted identifies an experimental // event that may change or be removed. SessionEventTypeAssistantFusionPhaseCompleted SessionEventType = "assistant.fusion_phase_completed" @@ -146,9 +149,12 @@ const ( SessionEventTypeSessionCanvasRemoved SessionEventType = "session.canvas.removed" // Experimental: SessionEventTypeSessionCanvasUnavailable identifies an experimental event // that may change or be removed. - SessionEventTypeSessionCanvasUnavailable SessionEventType = "session.canvas.unavailable" - SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" - SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" + SessionEventTypeSessionCanvasUnavailable SessionEventType = "session.canvas.unavailable" + SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" + SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" + // Experimental: SessionEventTypeSessionCompletionReceipt identifies an experimental event + // that may change or be removed. + SessionEventTypeSessionCompletionReceipt SessionEventType = "session.completion_receipt" SessionEventTypeSessionContextChanged SessionEventType = "session.context_changed" SessionEventTypeSessionContextCleared SessionEventType = "session.context_cleared" SessionEventTypeSessionCustomAgentsUpdated SessionEventType = "session.custom_agents_updated" @@ -474,6 +480,32 @@ func (*SessionAutopilotObjectiveChangedData) Type() SessionEventType { return SessionEventTypeSessionAutopilotObjectiveChanged } +// Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +// Experimental: SessionCompletionReceiptData is part of an experimental API and may change or be removed. +type SessionCompletionReceiptData struct { + // One-based accepted completion receipt ordinal in the durable session history. + Attempt int64 `json:"attempt"` + // Inclusive durable event range summarized by this receipt. + EventRange CompletionReceiptEventRange `json:"eventRange"` + // Number of failed structured tool completions in the covered range. + FailedToolCount int64 `json:"failedToolCount"` + // Final structured tool completion in the covered range, when one exists. + FinalTool *CompletionReceiptFinalTool `json:"finalTool,omitempty"` + // Version of the completion receipt payload. + SchemaVersion int64 `json:"schemaVersion"` + // Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + SourceEventID string `json:"sourceEventId"` + // Runtime reason the completion decision was accepted. + StopReason CompletionReceiptStopReason `json:"stopReason"` + // Number of successful structured tool completions in the covered range. + SuccessfulToolCount int64 `json:"successfulToolCount"` +} + +func (*SessionCompletionReceiptData) sessionEventData() {} +func (*SessionCompletionReceiptData) Type() SessionEventType { + return SessionEventTypeSessionCompletionReceipt +} + // Canonical bytes for a content-addressed binary asset shared by reference across events type SessionBinaryAssetData struct { // Content-addressed id for this binary asset (e.g. "sha256:..."). @@ -883,6 +915,34 @@ type SessionErrorData struct { func (*SessionErrorData) sessionEventData() {} func (*SessionErrorData) Type() SessionEventType { return SessionEventTypeSessionError } +// Experimental content-safe activity signal for a running HydraFusion phase. +// Experimental: AssistantFusionPhaseActivityData is part of an experimental API and may change or be removed. +type AssistantFusionPhaseActivityData struct { + // Kind of real activity observed. + Activity FusionPhaseActivityKind `json:"activity"` + // Conversation scope in which the phase executes. + ConversationScope FusionConversationScope `json:"conversationScope"` + // Identifier of the HydraFusion turn containing the phase. + FusionID string `json:"fusionId"` + // HydraFusion orchestration pattern containing the phase. + Pattern FusionPattern `json:"pattern"` + // Stable identifier for the concrete phase. + PhaseID string `json:"phaseId"` + // Kind of phase currently executing. + PhaseKind FusionPhaseKind `json:"phaseKind"` + // Semantic role assigned to the phase. + Role string `json:"role"` + // Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + ToolCallID *string `json:"toolCallId,omitempty"` + // Cumulative private response bytes observed for this model call. The event never includes response text. + TotalResponseSizeBytes *int64 `json:"totalResponseSizeBytes,omitempty"` +} + +func (*AssistantFusionPhaseActivityData) sessionEventData() {} +func (*AssistantFusionPhaseActivityData) Type() SessionEventType { + return SessionEventTypeAssistantFusionPhaseActivity +} + // Experimental durable HydraFusion phase output and lossless replay checkpoint. // Experimental: AssistantFusionPhaseCompletedData is part of an experimental API and may change or be removed. type AssistantFusionPhaseCompletedData struct { @@ -1045,6 +1105,9 @@ type SessionFusionResolvedData struct { ModelUniverseVersion *string `json:"modelUniverseVersion,omitempty"` // Validated orchestration pattern selected for the turn. Pattern FusionPattern `json:"pattern"` + // Presentation-neutral phase plan for clients that render workflow progress. + // Experimental: PhasePlan is part of an experimental API and may change or be removed. + PhasePlan []FusionPhasePlanStep `json:"phasePlan,omitzero"` // Version of the validated execution-plan format. PlanVersion *string `json:"planVersion,omitempty"` // HydraFusion routing policy used to resolve the plan. @@ -1817,6 +1880,8 @@ type UserMessageData struct { 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. IsAutopilotContinuation *bool `json:"isAutopilotContinuation,omitempty"` + // Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots + MessageID *string `json:"messageId,omitempty"` // Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitzero"` // Parent agent task ID for background telemetry correlated to this user turn @@ -1849,6 +1914,8 @@ func (*PermissionCompletedData) Type() SessionEventType { return SessionEventTyp // Permission request notification requiring client approval with request details type PermissionRequestedData struct { + // Agent mode captured from the owning turn when permission evaluation began. + AgentMode *SessionMode `json:"agentMode,omitempty"` // Details of the permission being requested PermissionRequest PermissionRequest `json:"permissionRequest"` // Derived user-facing permission prompt details for UI consumers @@ -2459,6 +2526,8 @@ type SubagentCompletedData struct { FirstDispatchedModel *string `json:"firstDispatchedModel,omitempty"` // Model used by the sub-agent Model *string `json:"model,omitempty"` + // Why an explicit task-call model did not become the effective model + ModelOverrideReason *string `json:"modelOverrideReason,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent ToolCallID string `json:"toolCallId"` // Total tokens (input + output) consumed by the sub-agent @@ -2492,6 +2561,8 @@ type SubagentFailedData struct { FirstDispatchedModel *string `json:"firstDispatchedModel,omitempty"` // Model selected for the sub-agent, when known Model *string `json:"model,omitempty"` + // Why an explicit task-call model did not become the effective model + ModelOverrideReason *string `json:"modelOverrideReason,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent ToolCallID string `json:"toolCallId"` // Total tokens (input + output) consumed before the sub-agent failed @@ -3123,7 +3194,27 @@ type CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail struct { TokenType string `json:"tokenType"` } -// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. +// Inclusive durable event range summarized by a completion receipt. +type CompletionReceiptEventRange struct { + // Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + EndEventID string `json:"endEventId"` + // Identifier of the user message that starts the covered exchange. + StartEventID string `json:"startEventId"` +} + +// Final structured tool completion in the covered event range. +type CompletionReceiptFinalTool struct { + // Process exit code from a structured shell result, when available. + ExitCode *int64 `json:"exitCode,omitempty"` + // Structured success or failure status from the tool completion event. + Status CompletionReceiptToolStatus `json:"status"` + // Unique identifier of the completed tool call. + ToolCallID string `json:"toolCallId"` + // Tool name from the matching tool execution start event, when available. + ToolName *string `json:"toolName,omitempty"` +} + +// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. type CustomAgentsUpdatedAgent struct { // Description of what the agent does Description string `json:"description"` @@ -3133,6 +3224,10 @@ type CustomAgentsUpdatedAgent struct { ID string `json:"id"` // Model override for this agent, if set Model *string `json:"model,omitempty"` + // Whether authored models are preferences or required constraints + ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` + // Authored model ids in priority order, if configured + Models []string `json:"models,omitzero"` // Internal name of the agent Name string `json:"name"` // Source location: user, project, inherited, remote, or plugin @@ -3209,6 +3304,19 @@ type FusionFollowUpRecommendation struct { UserTurn FusionFollowUpAction `json:"userTurn"` } +// Presentation-neutral phase planned for a HydraFusion turn. +// Experimental: FusionPhasePlanStep is part of an experimental API and may change or be removed. +type FusionPhasePlanStep struct { + // Whether the phase executes only when an earlier phase requests it. + Conditional bool `json:"conditional"` + // Kind of phase that may execute. + Kind FusionPhaseKind `json:"kind"` + // Semantic role assigned to the phase. + Role string `json:"role"` + // Conversation scope in which the phase executes. + Scope FusionConversationScope `json:"scope"` +} + // Aggregate concrete-model usage for one HydraFusion phase. // Experimental: FusionPhaseUsage is part of an experimental API and may change or be removed. type FusionPhaseUsage struct { @@ -5068,6 +5176,34 @@ const ( CompactionTriggerThreshold CompactionTrigger = "threshold" ) +// Runtime reason the completion decision was accepted. +type CompletionReceiptStopReason string + +const ( + // The configured agentStop continuation limit was reached. + CompletionReceiptStopReasonAgentStopBlockLimit CompletionReceiptStopReason = "agent_stop_block_limit" + // The model reached a natural terminal response. + CompletionReceiptStopReasonNatural CompletionReceiptStopReason = "natural" + // A terminal tool ended the interaction. + CompletionReceiptStopReasonTerminalTool CompletionReceiptStopReason = "terminal_tool" +) + +// Structured terminal status from a tool completion event. +type CompletionReceiptToolStatus string + +const ( + // The permissions service denied the tool call. + CompletionReceiptToolStatusDenied CompletionReceiptToolStatus = "denied" + // The tool failed without a more specific structured status. + CompletionReceiptToolStatusFailure CompletionReceiptToolStatus = "failure" + // The user rejected the tool call. + CompletionReceiptToolStatusRejected CompletionReceiptToolStatus = "rejected" + // The tool completed successfully. + CompletionReceiptToolStatusSuccess CompletionReceiptToolStatus = "success" + // The tool exceeded its time budget. + CompletionReceiptToolStatusTimeout CompletionReceiptToolStatus = "timeout" +) + // The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" (dismissed) type ElicitationCompletedAction string @@ -5198,6 +5334,19 @@ const ( FusionPatternSingle FusionPattern = "single" ) +// Content-safe activity observed while a HydraFusion phase is running. +// Experimental: FusionPhaseActivityKind is part of an experimental API and may change or be removed. +type FusionPhaseActivityKind string + +const ( + // The provider produced additional private output bytes. + FusionPhaseActivityKindModelOutput FusionPhaseActivityKind = "model_output" + // A tool finished executing inside the phase. + FusionPhaseActivityKindToolCompleted FusionPhaseActivityKind = "tool_completed" + // A tool began executing inside the phase. + FusionPhaseActivityKindToolStarted FusionPhaseActivityKind = "tool_started" +) + // HydraFusion phase kind. // Experimental: FusionPhaseKind is part of an experimental API and may change or be removed. type FusionPhaseKind string diff --git a/go/zsession_events.go b/go/zsession_events.go index 360f1a7588..8e8371cade 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -12,6 +12,8 @@ type ( AgentInterruptedActivity = rpc.AgentInterruptedActivity AgentInterruptedCancelPhase = rpc.AgentInterruptedCancelPhase AgentInterruptedData = rpc.AgentInterruptedData + AgentModelPolicy = rpc.AgentModelPolicy + AssistantFusionPhaseActivityData = rpc.AssistantFusionPhaseActivityData AssistantFusionPhaseCompletedData = rpc.AssistantFusionPhaseCompletedData AssistantFusionPhaseFailedData = rpc.AssistantFusionPhaseFailedData AssistantFusionPhaseStartedData = rpc.AssistantFusionPhaseStartedData @@ -97,6 +99,10 @@ type ( CompactionCompleteCompactionTokensUsed = rpc.CompactionCompleteCompactionTokensUsed CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail = rpc.CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail CompactionTrigger = rpc.CompactionTrigger + CompletionReceiptEventRange = rpc.CompletionReceiptEventRange + CompletionReceiptFinalTool = rpc.CompletionReceiptFinalTool + CompletionReceiptStopReason = rpc.CompletionReceiptStopReason + CompletionReceiptToolStatus = rpc.CompletionReceiptToolStatus ContextTier = rpc.ContextTier CustomAgentsUpdatedAgent = rpc.CustomAgentsUpdatedAgent ElicitationCompletedAction = rpc.ElicitationCompletedAction @@ -126,7 +132,9 @@ type ( FusionFollowUpAction = rpc.FusionFollowUpAction FusionFollowUpRecommendation = rpc.FusionFollowUpRecommendation FusionPattern = rpc.FusionPattern + FusionPhaseActivityKind = rpc.FusionPhaseActivityKind FusionPhaseKind = rpc.FusionPhaseKind + FusionPhasePlanStep = rpc.FusionPhasePlanStep FusionPhaseStatus = rpc.FusionPhaseStatus FusionPhaseUsage = rpc.FusionPhaseUsage FusionScores = rpc.FusionScores @@ -265,6 +273,7 @@ type ( SessionCanvasUnavailableData = rpc.SessionCanvasUnavailableData SessionCompactionCompleteData = rpc.SessionCompactionCompleteData SessionCompactionStartData = rpc.SessionCompactionStartData + SessionCompletionReceiptData = rpc.SessionCompletionReceiptData SessionContextChangedData = rpc.SessionContextChangedData SessionContextClearedData = rpc.SessionContextClearedData SessionCustomAgentsUpdatedData = rpc.SessionCustomAgentsUpdatedData @@ -427,6 +436,8 @@ const ( AgentInterruptedActivityToolCall = rpc.AgentInterruptedActivityToolCall AgentInterruptedCancelPhaseMidStream = rpc.AgentInterruptedCancelPhaseMidStream AgentInterruptedCancelPhasePreFirstToken = rpc.AgentInterruptedCancelPhasePreFirstToken + AgentModelPolicyPreferred = rpc.AgentModelPolicyPreferred + AgentModelPolicyRequired = rpc.AgentModelPolicyRequired AssistantMessageToolRequestCallerTypeProgram = rpc.AssistantMessageToolRequestCallerTypeProgram AssistantMessageToolRequestTypeCustom = rpc.AssistantMessageToolRequestTypeCustom AssistantMessageToolRequestTypeFunction = rpc.AssistantMessageToolRequestTypeFunction @@ -491,6 +502,14 @@ const ( CompactionTriggerMemoryPressure = rpc.CompactionTriggerMemoryPressure CompactionTriggerModelSwitch = rpc.CompactionTriggerModelSwitch CompactionTriggerThreshold = rpc.CompactionTriggerThreshold + CompletionReceiptStopReasonAgentStopBlockLimit = rpc.CompletionReceiptStopReasonAgentStopBlockLimit + CompletionReceiptStopReasonNatural = rpc.CompletionReceiptStopReasonNatural + CompletionReceiptStopReasonTerminalTool = rpc.CompletionReceiptStopReasonTerminalTool + CompletionReceiptToolStatusDenied = rpc.CompletionReceiptToolStatusDenied + CompletionReceiptToolStatusFailure = rpc.CompletionReceiptToolStatusFailure + CompletionReceiptToolStatusRejected = rpc.CompletionReceiptToolStatusRejected + CompletionReceiptToolStatusSuccess = rpc.CompletionReceiptToolStatusSuccess + CompletionReceiptToolStatusTimeout = rpc.CompletionReceiptToolStatusTimeout ContextTierDefault = rpc.ContextTierDefault ContextTierLongContext = rpc.ContextTierLongContext ElicitationCompletedActionAccept = rpc.ElicitationCompletedActionAccept @@ -524,6 +543,9 @@ const ( FusionPatternCascade = rpc.FusionPatternCascade FusionPatternCritique = rpc.FusionPatternCritique FusionPatternSingle = rpc.FusionPatternSingle + FusionPhaseActivityKindModelOutput = rpc.FusionPhaseActivityKindModelOutput + FusionPhaseActivityKindToolCompleted = rpc.FusionPhaseActivityKindToolCompleted + FusionPhaseActivityKindToolStarted = rpc.FusionPhaseActivityKindToolStarted FusionPhaseKindCritic = rpc.FusionPhaseKindCritic FusionPhaseKindDraft = rpc.FusionPhaseKindDraft FusionPhaseKindFollowUp = rpc.FusionPhaseKindFollowUp @@ -671,6 +693,7 @@ const ( ScheduleOriginUser = rpc.ScheduleOriginUser SessionEventTypeAbort = rpc.SessionEventTypeAbort SessionEventTypeAgentInterrupted = rpc.SessionEventTypeAgentInterrupted + SessionEventTypeAssistantFusionPhaseActivity = rpc.SessionEventTypeAssistantFusionPhaseActivity SessionEventTypeAssistantFusionPhaseCompleted = rpc.SessionEventTypeAssistantFusionPhaseCompleted SessionEventTypeAssistantFusionPhaseFailed = rpc.SessionEventTypeAssistantFusionPhaseFailed SessionEventTypeAssistantFusionPhaseStarted = rpc.SessionEventTypeAssistantFusionPhaseStarted @@ -737,6 +760,7 @@ const ( SessionEventTypeSessionCanvasUnavailable = rpc.SessionEventTypeSessionCanvasUnavailable SessionEventTypeSessionCompactionComplete = rpc.SessionEventTypeSessionCompactionComplete SessionEventTypeSessionCompactionStart = rpc.SessionEventTypeSessionCompactionStart + SessionEventTypeSessionCompletionReceipt = rpc.SessionEventTypeSessionCompletionReceipt SessionEventTypeSessionContextChanged = rpc.SessionEventTypeSessionContextChanged SessionEventTypeSessionContextCleared = rpc.SessionEventTypeSessionContextCleared SessionEventTypeSessionCustomAgentsUpdated = rpc.SessionEventTypeSessionCustomAgentsUpdated diff --git a/java/pom.xml b/java/pom.xml index f053f39ad7..7bbc1c8ce6 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -63,7 +63,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.83-1 + ^1.0.83-2 true diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 8d3292a1b0..31e908aca7 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.83-1", + "@github/copilot": "^1.0.83-2", "json-schema": "^0.4.0", "tsx": "^4.23.13" } @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-1", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-1.tgz", - "integrity": "sha512-7rhgbgSx7IgfFYXrTx2/2+qowv6MZjC6BN0mhba37RuGQxsca9Zk2ucAeIX3GbQTp91AhvnN0aHCxabX36xADQ==", + "version": "1.0.83-2", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-2.tgz", + "integrity": "sha512-ntRvwGdZbZJjOvdV3LVBQ7z69W19DO4MWe/dh6pP8kYeumbzC4udVj7mCtoSwBOs0hwXirhXY+BZwqSJUp5FLQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-1", - "@github/copilot-darwin-x64": "1.0.83-1", - "@github/copilot-linux-arm64": "1.0.83-1", - "@github/copilot-linux-x64": "1.0.83-1", - "@github/copilot-linuxmusl-arm64": "1.0.83-1", - "@github/copilot-linuxmusl-x64": "1.0.83-1", - "@github/copilot-win32-arm64": "1.0.83-1", - "@github/copilot-win32-x64": "1.0.83-1" + "@github/copilot-darwin-arm64": "1.0.83-2", + "@github/copilot-darwin-x64": "1.0.83-2", + "@github/copilot-linux-arm64": "1.0.83-2", + "@github/copilot-linux-x64": "1.0.83-2", + "@github/copilot-linuxmusl-arm64": "1.0.83-2", + "@github/copilot-linuxmusl-x64": "1.0.83-2", + "@github/copilot-win32-arm64": "1.0.83-2", + "@github/copilot-win32-x64": "1.0.83-2" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-1", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-1.tgz", - "integrity": "sha512-gxYJEd4yoIBIQHRmJw6ZegSkR6VuNm7AJVdAdARcU0SS68VZjtZFT29luThAp75Pxp37S5w+3A/jQNtxGyHizw==", + "version": "1.0.83-2", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-2.tgz", + "integrity": "sha512-RBjF/zTJe+gp9PsthYtIHF36+y3A3Zv3w/2FAa9nHF6ople1+lYoQ0OC1z6tWQSpBx3n/fSkNt0ebMmfhWlQyw==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-1", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-1.tgz", - "integrity": "sha512-PL63+Ib1RH+Bpu2UYJ9E8tmD4NZD/YlhD25F1Q9Rb86AdgLEoRYlxjNxXXF9P9dMmJ/u5zm0rUtWIRxv/FRbPg==", + "version": "1.0.83-2", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-2.tgz", + "integrity": "sha512-GzaarluCiHUA4yrYxWIFEklwPLTYjxGvLyYjI0wW5IPseT7GLFjN7AkDGRU4Ny/mxTj1MbUEsJAWVtiNy5EUHg==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-1.tgz", - "integrity": "sha512-7c4RVEuXQu9zepAWoXwkizpESztUbWUR1hLPl+o+19L+D8fXm+5XQRH5E22Ia7P7/xHW3Ml2o2x5cXFq0k7/7w==", + "version": "1.0.83-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-2.tgz", + "integrity": "sha512-iGD5Pc7vnzrAjqTK/BrI8MDtMqXyxUe7P6u8TiGt+4DzKe3v2myT6eFQ+z/JJOHNpiu6LUpTDzwS8WttZqqJpA==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-1.tgz", - "integrity": "sha512-Be0zUVysLYV66pnOPhMxpShQ8Ox5V07qcsSOoNzG7iwpzkfZuf70U9CHk/fnI//mWwmZR+caPUfVM9BpwrdNnA==", + "version": "1.0.83-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-2.tgz", + "integrity": "sha512-jOQwz075vRWat+RJiMNtD+7vm4DQ83Dvw4iakpNkLfqWMPXlliVr/tqL+SffeSo87LnhOKobZOJOjELS27drdg==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-1.tgz", - "integrity": "sha512-PZNP8vZZJU8yIc3dNjGUBUCFPNjqZs8FPZLDxvL6XdHM7Ep4cLBa+QdbRsw+nZONIaG0GtQf26eqk5ymYYpUPA==", + "version": "1.0.83-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-2.tgz", + "integrity": "sha512-6/+ByEBEV/vDofrCymaKrEZ5/p+TVHQfgWu8ywvWEPALfxj/iHpAF3grM2bj7uED4C19LTSarHNCkCqdrAUzIQ==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-1.tgz", - "integrity": "sha512-uKMhGtg5K07nA5/pxDSq8S5F/I+sMgVhUXg90Y4BI9trOidgxpXpCVyh4TeRNj2ENNugPvATdfGc6eFPsjmlgQ==", + "version": "1.0.83-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-2.tgz", + "integrity": "sha512-8dbiPUHaDemDibLfKXDe5xublJxt6fOht4yYDk/ikmOGAVezBUwRVO27PchXQBwGjP2PVNAASkv2WB4Ubw72lA==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-1", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-1.tgz", - "integrity": "sha512-vMxHTmv3SotBuNI4LKcIFckd+n0OvhTik27qjsqOnVqFpvmHDghSsa8YPxySyxUQIE35z5pJaivVBK3dfEAY7w==", + "version": "1.0.83-2", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-2.tgz", + "integrity": "sha512-ZE1iUXlJSnNIH+VoRG96emy2e7jMJm85/pcKCrgLvqhDAcQSqiqLVv3OJp2VX2IcDxToOpiaGccU8Ab9zHQqkQ==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-1", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-1.tgz", - "integrity": "sha512-2l0VVTazW3/+p94ZQ5sAPCj1iH7KWxJAYh4yXWfvpsB/1JEEmyVgpLei1MeaMFVdtp/hi9bGprZ9+zWNlHh5hQ==", + "version": "1.0.83-2", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-2.tgz", + "integrity": "sha512-qfturLon+1oaWqSaXCISL6BEf5K+ijJFl0m/OZwstWYSlIlaE2+iOz5LcRukoxm25OG1JZDHQFp1k2PY2A+LbA==", "cpu": [ "x64" ], diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 4e7c52ff8a..f5b28608a9 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.83-1", + "@github/copilot": "^1.0.83-2", "json-schema": "^0.4.0", "tsx": "^4.23.13" } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.java b/java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.java new file mode 100644 index 0000000000..06c80ba22e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Whether configured models are advisory preferences or required constraints + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AgentModelPolicy { + /** The {@code preferred} variant. */ + PREFERRED("preferred"), + /** The {@code required} variant. */ + REQUIRED("required"); + + private final String value; + AgentModelPolicy(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AgentModelPolicy fromValue(String value) { + for (AgentModelPolicy v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AgentModelPolicy value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.java new file mode 100644 index 0000000000..64d8d58283 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantFusionPhaseActivityEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.fusion_phase_activity"; } + + @JsonProperty("data") + private AssistantFusionPhaseActivityEventData data; + + public AssistantFusionPhaseActivityEventData getData() { return data; } + public void setData(AssistantFusionPhaseActivityEventData data) { this.data = data; } + + /** Data payload for {@link AssistantFusionPhaseActivityEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantFusionPhaseActivityEventData( + /** Identifier of the HydraFusion turn containing the phase. */ + @JsonProperty("fusionId") String fusionId, + /** Stable identifier for the concrete phase. */ + @JsonProperty("phaseId") String phaseId, + /** Kind of phase currently executing. */ + @JsonProperty("phaseKind") FusionPhaseKind phaseKind, + /** HydraFusion orchestration pattern containing the phase. */ + @JsonProperty("pattern") FusionPattern pattern, + /** Semantic role assigned to the phase. */ + @JsonProperty("role") String role, + /** Conversation scope in which the phase executes. */ + @JsonProperty("conversationScope") FusionConversationScope conversationScope, + /** Kind of real activity observed. */ + @JsonProperty("activity") FusionPhaseActivityKind activity, + /** Cumulative private response bytes observed for this model call. The event never includes response text. */ + @JsonProperty("totalResponseSizeBytes") Long totalResponseSizeBytes, + /** Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. */ + @JsonProperty("toolCallId") String toolCallId + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.java new file mode 100644 index 0000000000..7b05d1c9cd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.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; + +/** + * Inclusive durable event range summarized by a completion receipt. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CompletionReceiptEventRange( + /** Identifier of the user message that starts the covered exchange. */ + @JsonProperty("startEventId") String startEventId, + /** Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. */ + @JsonProperty("endEventId") String endEventId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.java new file mode 100644 index 0000000000..74d78d7ee1 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Final structured tool completion in the covered event range. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CompletionReceiptFinalTool( + /** Unique identifier of the completed tool call. */ + @JsonProperty("toolCallId") String toolCallId, + /** Tool name from the matching tool execution start event, when available. */ + @JsonProperty("toolName") String toolName, + /** Structured success or failure status from the tool completion event. */ + @JsonProperty("status") CompletionReceiptToolStatus status, + /** Process exit code from a structured shell result, when available. */ + @JsonProperty("exitCode") Long exitCode +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.java new file mode 100644 index 0000000000..49874b0275 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.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; + +/** + * Runtime reason the completion decision was accepted. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum CompletionReceiptStopReason { + /** The {@code natural} variant. */ + NATURAL("natural"), + /** The {@code terminal_tool} variant. */ + TERMINAL_TOOL("terminal_tool"), + /** The {@code agent_stop_block_limit} variant. */ + AGENT_STOP_BLOCK_LIMIT("agent_stop_block_limit"); + + private final String value; + CompletionReceiptStopReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CompletionReceiptStopReason fromValue(String value) { + for (CompletionReceiptStopReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown CompletionReceiptStopReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.java new file mode 100644 index 0000000000..e82a1e6986 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.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 javax.annotation.processing.Generated; + +/** + * Structured terminal status from a tool completion event. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum CompletionReceiptToolStatus { + /** The {@code success} variant. */ + SUCCESS("success"), + /** The {@code failure} variant. */ + FAILURE("failure"), + /** The {@code timeout} variant. */ + TIMEOUT("timeout"), + /** The {@code rejected} variant. */ + REJECTED("rejected"), + /** The {@code denied} variant. */ + DENIED("denied"); + + private final String value; + CompletionReceiptToolStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CompletionReceiptToolStatus fromValue(String value) { + for (CompletionReceiptToolStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown CompletionReceiptToolStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java b/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java index c2f195e486..762f0b1ac8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. + * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. * * @since 1.0.0 */ @@ -37,6 +37,10 @@ public record CustomAgentsUpdatedAgent( /** Whether the agent can be selected by the user */ @JsonProperty("userInvocable") Boolean userInvocable, /** Model override for this agent, if set */ - @JsonProperty("model") String model + @JsonProperty("model") String model, + /** Authored model ids in priority order, if configured */ + @JsonProperty("models") List models, + /** Whether authored models are preferences or required constraints */ + @JsonProperty("modelPolicy") AgentModelPolicy modelPolicy ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java new file mode 100644 index 0000000000..a95a08cf91 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.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; + +/** + * Content-safe activity observed while a HydraFusion phase is running. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum FusionPhaseActivityKind { + /** The {@code model_output} variant. */ + MODEL_OUTPUT("model_output"), + /** The {@code tool_started} variant. */ + TOOL_STARTED("tool_started"), + /** The {@code tool_completed} variant. */ + TOOL_COMPLETED("tool_completed"); + + private final String value; + FusionPhaseActivityKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static FusionPhaseActivityKind fromValue(String value) { + for (FusionPhaseActivityKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown FusionPhaseActivityKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java new file mode 100644 index 0000000000..cfb07a789a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Presentation-neutral phase planned for a HydraFusion turn. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record FusionPhasePlanStep( + /** Kind of phase that may execute. */ + @JsonProperty("kind") FusionPhaseKind kind, + /** Semantic role assigned to the phase. */ + @JsonProperty("role") String role, + /** Conversation scope in which the phase executes. */ + @JsonProperty("scope") FusionConversationScope scope, + /** Whether the phase executes only when an earlier phase requests it. */ + @JsonProperty("conditional") Boolean conditional +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java index b7aae9ec80..a854d5ffa4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java @@ -40,6 +40,8 @@ public record PermissionRequestedEventData( @JsonProperty("permissionRequest") Object permissionRequest, /** Derived user-facing permission prompt details for UI consumers */ @JsonProperty("promptRequest") Object promptRequest, + /** Agent mode captured from the owning turn when permission evaluation began. */ + @JsonProperty("agentMode") SessionMode agentMode, /** Neutral risk metadata supplied by the tool host. Consumers may display this value but must not use it to bypass the permission decision. */ @JsonProperty("riskAssessment") Object riskAssessment, /** When true, this permission was already resolved by a permissionRequest hook and requires no client action */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java new file mode 100644 index 0000000000..15670c992a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * 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 "session.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionCompletionReceiptEvent extends SessionEvent { + + @Override + public String getType() { return "session.completion_receipt"; } + + @JsonProperty("data") + private SessionCompletionReceiptEventData data; + + public SessionCompletionReceiptEventData getData() { return data; } + public void setData(SessionCompletionReceiptEventData data) { this.data = data; } + + /** Data payload for {@link SessionCompletionReceiptEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionCompletionReceiptEventData( + /** Version of the completion receipt payload. */ + @JsonProperty("schemaVersion") Long schemaVersion, + /** One-based accepted completion receipt ordinal in the durable session history. */ + @JsonProperty("attempt") Long attempt, + /** Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. */ + @JsonProperty("sourceEventId") String sourceEventId, + /** Inclusive durable event range summarized by this receipt. */ + @JsonProperty("eventRange") CompletionReceiptEventRange eventRange, + /** Runtime reason the completion decision was accepted. */ + @JsonProperty("stopReason") CompletionReceiptStopReason stopReason, + /** Final structured tool completion in the covered range, when one exists. */ + @JsonProperty("finalTool") CompletionReceiptFinalTool finalTool, + /** Number of successful structured tool completions in the covered range. */ + @JsonProperty("successfulToolCount") Long successfulToolCount, + /** Number of failed structured tool completions in the covered range. */ + @JsonProperty("failedToolCount") Long failedToolCount + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java index bf5dc65769..193aa56b8f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -56,6 +56,7 @@ @JsonSubTypes.Type(value = SessionCompactionStartEvent.class, name = "session.compaction_start"), @JsonSubTypes.Type(value = SessionCompactionCompleteEvent.class, name = "session.compaction_complete"), @JsonSubTypes.Type(value = SessionTaskCompleteEvent.class, name = "session.task_complete"), + @JsonSubTypes.Type(value = SessionCompletionReceiptEvent.class, name = "session.completion_receipt"), @JsonSubTypes.Type(value = SessionFusionRouteStartedEvent.class, name = "session.fusion_route_started"), @JsonSubTypes.Type(value = SessionFusionRouteFailedEvent.class, name = "session.fusion_route_failed"), @JsonSubTypes.Type(value = SessionFusionResolvedEvent.class, name = "session.fusion_resolved"), @@ -67,6 +68,7 @@ @JsonSubTypes.Type(value = AgentInterruptedEvent.class, name = "agent.interrupted"), @JsonSubTypes.Type(value = AssistantIntentEvent.class, name = "assistant.intent"), @JsonSubTypes.Type(value = AssistantFusionPhaseStartedEvent.class, name = "assistant.fusion_phase_started"), + @JsonSubTypes.Type(value = AssistantFusionPhaseActivityEvent.class, name = "assistant.fusion_phase_activity"), @JsonSubTypes.Type(value = AssistantFusionPhaseCompletedEvent.class, name = "assistant.fusion_phase_completed"), @JsonSubTypes.Type(value = AssistantFusionPhaseFailedEvent.class, name = "assistant.fusion_phase_failed"), @JsonSubTypes.Type(value = AssistantServerToolProgressEvent.class, name = "assistant.server_tool_progress"), @@ -190,6 +192,7 @@ public abstract sealed class SessionEvent permits SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionTaskCompleteEvent, + SessionCompletionReceiptEvent, SessionFusionRouteStartedEvent, SessionFusionRouteFailedEvent, SessionFusionResolvedEvent, @@ -201,6 +204,7 @@ public abstract sealed class SessionEvent permits AgentInterruptedEvent, AssistantIntentEvent, AssistantFusionPhaseStartedEvent, + AssistantFusionPhaseActivityEvent, AssistantFusionPhaseCompletedEvent, AssistantFusionPhaseFailedEvent, AssistantServerToolProgressEvent, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java index 7553c6eb43..d63bee33b3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java @@ -10,6 +10,7 @@ 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; /** @@ -62,6 +63,8 @@ public record SessionFusionResolvedEventData( @JsonProperty("scores") FusionScores scores, /** Validated orchestration pattern selected for the turn. */ @JsonProperty("pattern") FusionPattern pattern, + /** Presentation-neutral phase plan for clients that render workflow progress. */ + @JsonProperty("phasePlan") List phasePlan, /** Concrete model selected for the primary solver phase. */ @JsonProperty("primaryModel") String primaryModel, /** Concrete model selected for the review or judge phase, when required. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java index 62f6803652..26459bb955 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java @@ -50,6 +50,8 @@ public record SubagentCompletedEventData( @JsonProperty("explicitModelOverride") String explicitModelOverride, /** Whether the explicit task-call model matched the user's configured preference */ @JsonProperty("explicitModelMatchesPreference") Boolean explicitModelMatchesPreference, + /** Why an explicit task-call model did not become the effective model */ + @JsonProperty("modelOverrideReason") String modelOverrideReason, /** Whether the first model actually dispatched matched the user's configured preference */ @JsonProperty("configuredModelMatchesActual") Boolean configuredModelMatchesActual, /** Total number of tool calls made by the sub-agent */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java index 1d1413c64d..54464f8ddf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java @@ -52,6 +52,8 @@ public record SubagentFailedEventData( @JsonProperty("explicitModelOverride") String explicitModelOverride, /** Whether the explicit task-call model matched the user's configured preference */ @JsonProperty("explicitModelMatchesPreference") Boolean explicitModelMatchesPreference, + /** Why an explicit task-call model did not become the effective model */ + @JsonProperty("modelOverrideReason") String modelOverrideReason, /** Whether the first model actually dispatched matched the user's configured preference */ @JsonProperty("configuredModelMatchesActual") Boolean configuredModelMatchesActual, /** Total number of tool calls made before the sub-agent failed */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java index 7d839087da..43a886ea01 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java @@ -37,6 +37,8 @@ public final class UserMessageEvent extends SessionEvent { public record UserMessageEventData( /** The user's message text as displayed in the timeline */ @JsonProperty("content") String content, + /** Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots */ + @JsonProperty("messageId") String messageId, /** Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching */ @JsonProperty("transformedContent") String transformedContent, /** Files, selections, or GitHub references attached to the message */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java index f239c82e61..3d9f9c2d7e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. + * Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. * * @since 1.0.0 */ @@ -41,6 +41,10 @@ public record AgentInfo( @JsonProperty("tools") List tools, /** Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. */ @JsonProperty("model") String model, + /** Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. */ + @JsonProperty("models") List models, + /** Whether authored models are preferences or required constraints. */ + @JsonProperty("modelPolicy") AgentModelPolicy modelPolicy, /** MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. */ @JsonProperty("mcpServers") Map mcpServers, /** Skill names preloaded into this agent's context. Omitted means none. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.java new file mode 100644 index 0000000000..99158516f0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Whether configured models are advisory preferences or required constraints + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AgentModelPolicy { + /** The {@code preferred} variant. */ + PREFERRED("preferred"), + /** The {@code required} variant. */ + REQUIRED("required"); + + private final String value; + AgentModelPolicy(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AgentModelPolicy fromValue(String value) { + for (AgentModelPolicy v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AgentModelPolicy value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.java new file mode 100644 index 0000000000..fce707243d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * One server-discovered hook action from user, repository, plugin, or managed-policy configuration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record DiscoveredHook( + /** Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. */ + @JsonProperty("id") String id, + /** Hook event that invokes this action. */ + @JsonProperty("hookType") HookType hookType, + /** Configuration tier that contributed this hook action. */ + @JsonProperty("origin") HookOrigin origin, + /** Human-readable source label, such as a hook file path, settings source, or plugin name. */ + @JsonProperty("source") String source, + /** Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. */ + @JsonProperty("projectPath") String projectPath, + /** Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. */ + @JsonProperty("enabled") Boolean enabled, + /** Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. */ + @JsonProperty("disableKey") String disableKey +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.java new file mode 100644 index 0000000000..bf3bfd14cd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Configuration tier that contributed a discovered hook action. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum HookOrigin { + /** The {@code user} variant. */ + USER("user"), + /** The {@code repository} variant. */ + REPOSITORY("repository"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"), + /** The {@code policy} variant. */ + POLICY("policy"); + + private final String value; + HookOrigin(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static HookOrigin fromValue(String value) { + for (HookOrigin v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown HookOrigin value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java index 8d7cd913c3..1a185958de 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Hook event name dispatched through the SDK callback transport. + * Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. * * @since 1.0.0 */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.java new file mode 100644 index 0000000000..a0e7610394 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * @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 HooksDiscoverParams( + /** Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. */ + @JsonProperty("projectPaths") List projectPaths, + /** When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. */ + @JsonProperty("excludeHostHooks") Boolean excludeHostHooks +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.java new file mode 100644 index 0000000000..f30d4837ae --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + * + * @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 HooksDiscoverResult( + /** All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. */ + @JsonProperty("hooks") List hooks, + /** Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. */ + @JsonProperty("warnings") List warnings, + /** Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. */ + @JsonProperty("errors") List errors +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java index f3b2f99188..f815f565f5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java @@ -23,6 +23,8 @@ public record QueuePendingItems( /** Stable opaque id for the canonical queued item. Batch rows share one id. */ @JsonProperty("id") String id, + /** Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. */ + @JsonProperty("messageId") String messageId, /** Whether this item is a queued user message or a queued slash command / model change */ @JsonProperty("kind") QueuePendingItemsKind kind, /** Human-readable text to display for this queue entry in the UI */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java index 1e56acb53f..9c5e3e475f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java @@ -25,7 +25,7 @@ public record SandboxConfigUserPolicyNetwork( @JsonProperty("allowOutbound") Boolean allowOutbound, /** Whether traffic to local/loopback addresses is allowed. */ @JsonProperty("allowLocalNetwork") Boolean allowLocalNetwork, - /** HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. */ + /** HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is accepted and routed through the IPv4 gateway), and does not support proxy credentials. macOS relies on applications honoring proxy environment variables. Windows also configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's networking stack. Configure supported credentials in the separate `username` and `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, while an https:// or authenticated loopback URL uses the URL form. */ @JsonProperty("proxy") SandboxConfigUserPolicyNetworkProxy proxy ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java index 74ff86919e..2946269ec1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SandboxConfigUserPolicyNetworkProxy( - /** Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ + /** Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ @JsonProperty("url") String url, /** Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. */ @JsonProperty("username") String username, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java new file mode 100644 index 0000000000..b6f69bf7dc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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 hooks} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerHooksApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerHooksApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture discover(HooksDiscoverParams params) { + return caller.invoke("hooks.discover", params, HooksDiscoverResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java index 111cee2560..caa7f92d07 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java @@ -25,6 +25,8 @@ public final class ServerRpc { private final RpcCaller caller; + /** API methods for the {@code hooks} namespace. */ + public final ServerHooksApi hooks; /** API methods for the {@code models} namespace. */ public final ServerModelsApi models; /** API methods for the {@code tools} namespace. */ @@ -71,6 +73,7 @@ public final class ServerRpc { */ public ServerRpc(RpcCaller caller) { this.caller = caller; + this.hooks = new ServerHooksApi(caller); this.models = new ServerModelsApi(caller); this.tools = new ServerToolsApi(caller); this.account = new ServerAccountApi(caller); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java index 52481a7d73..870f7ac3cf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java @@ -94,6 +94,17 @@ public CompletableFuture getMetadata(SessionsGetMetad return caller.invoke("sessions.getMetadata", params, SessionsGetMetadataResult.class); } + /** + * Pagination options for reading an inactive or active local session's persisted event journal. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture readPersistedEvents(SessionsReadPersistedEventsParams params) { + return caller.invoke("sessions.readPersistedEvents", params, SessionsReadPersistedEventsResult.class); + } + /** * Limit for non-empty local session IDs. * diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java index a7c60d28cd..d7e36cf3c9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java @@ -38,7 +38,7 @@ public record SessionModelSwitchToParams( @JsonProperty("modelCapabilities") ModelCapabilitiesOverride modelCapabilities, /** Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. */ @JsonProperty("contextTier") ContextTier contextTier, - /** Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. */ + /** Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. */ @JsonProperty("source") ModelChangeSource source, /** When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). */ @JsonProperty("deferIfModelChangeQueued") Boolean deferIfModelChangeQueued, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.java new file mode 100644 index 0000000000..4da93409be --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Pagination options for reading an inactive or active local session's persisted event journal. + * + * @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 SessionsReadPersistedEventsParams( + /** Session ID whose persisted event journal should be read. */ + @JsonProperty("sessionId") String sessionId, + /** Opaque cursor returned by a previous persisted-event read. Omit on the first call. */ + @JsonProperty("cursor") String cursor, + /** Maximum number of events to return in this batch (1–1000, default 200). */ + @JsonProperty("max") Long max, + /** Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. */ + @JsonProperty("direction") EventsReadDirection direction +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.java new file mode 100644 index 0000000000..f022df5ae2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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 com.github.copilot.generated.SessionEvent; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Batch of session events returned by a read, with cursor and continuation metadata. + * + * @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 SessionsReadPersistedEventsResult( + /** Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. */ + @JsonProperty("events") List events, + /** Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). */ + @JsonProperty("cursor") String cursor, + /** True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. */ + @JsonProperty("hasMore") Boolean hasMore, + /** Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. */ + @JsonProperty("cursorStatus") EventsCursorStatus cursorStatus +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java index 29426d931b..4a366864c3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java @@ -23,6 +23,8 @@ public record SubagentSettingsEntry( /** Model override for matching subagents */ @JsonProperty("model") String model, + /** Whether the configured model strategy is preferred or required */ + @JsonProperty("modelPolicy") AgentModelPolicy modelPolicy, /** Reasoning effort override for matching subagents */ @JsonProperty("effortLevel") String effortLevel, /** Context tier override for matching subagents */ diff --git a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java index 18045f3e86..f7fc58d429 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java @@ -142,7 +142,7 @@ void testShouldUpdateAndClearLiveSubagentSettings() throws Exception { session.getRpc().tools.updateSubagentSettings(new SessionToolsUpdateSubagentSettingsParams(null, new SessionToolsUpdateSubagentSettingsParams.SessionToolsUpdateSubagentSettingsParamsSubagents( Map.of("general-purpose", - new SubagentSettingsEntry("gpt-5-mini", "low", + new SubagentSettingsEntry("gpt-5-mini", null, "low", SubagentSettingsEntryContextTier.LONG_CONTEXT)), List.of("legacy-agent"), null, null))) .get(30, TimeUnit.SECONDS); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index eb0bc3d723..f86b3dfbcb 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -470,7 +470,7 @@ void pingResult_fields() { @Test void sessionAgentListResult_with_items() { var item = new AgentInfo("name1", "Name One", "Desc 1", "/path/to/agent1", null, null, null, null, null, null, - null, null); + null, null, null, null); var result = new SessionAgentListResult(List.of(item)); assertEquals(1, result.agents().size()); assertEquals("name1", result.agents().get(0).name()); @@ -482,7 +482,7 @@ void sessionAgentListResult_with_items() { @Test void sessionAgentGetCurrentResult_nested() { var agent = new AgentInfo("agent-1", "Agent One", "Does things", null, null, null, null, null, null, null, null, - null); + null, null, null); var result = new SessionAgentGetCurrentResult(agent); assertEquals("agent-1", result.agent().name()); assertEquals("Agent One", result.agent().displayName()); @@ -498,7 +498,8 @@ void sessionAgentGetCurrentResult_null_agent() { @Test void sessionAgentReloadResult_with_items() { - var item = new AgentInfo("a", "A", "Desc", "/path/to/a", null, null, null, null, null, null, null, null); + var item = new AgentInfo("a", "A", "Desc", "/path/to/a", null, null, null, null, null, null, null, null, null, + null); var result = new SessionAgentReloadResult(List.of(item)); assertEquals(1, result.agents().size()); assertEquals("a", result.agents().get(0).name()); @@ -507,7 +508,7 @@ void sessionAgentReloadResult_with_items() { @Test void sessionAgentSelectResult_nested() { var agent = new AgentInfo("selected", "Selected", "The selected agent", "/path/to/selected", null, null, null, - null, null, null, null, null); + null, null, null, null, null, null, null); var result = new SessionAgentSelectResult(agent); assertEquals("selected", result.agent().name()); } diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 39bb781f7b..39863e29a3 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-1", + "@github/copilot": "^1.0.83-2", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -658,8 +658,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-1", - "integrity": "sha512-7rhgbgSx7IgfFYXrTx2/2+qowv6MZjC6BN0mhba37RuGQxsca9Zk2ucAeIX3GbQTp91AhvnN0aHCxabX36xADQ==", + "version": "1.0.83-2", + "integrity": "sha512-ntRvwGdZbZJjOvdV3LVBQ7z69W19DO4MWe/dh6pP8kYeumbzC4udVj7mCtoSwBOs0hwXirhXY+BZwqSJUp5FLQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -668,19 +668,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-1", - "@github/copilot-darwin-x64": "1.0.83-1", - "@github/copilot-linux-arm64": "1.0.83-1", - "@github/copilot-linux-x64": "1.0.83-1", - "@github/copilot-linuxmusl-arm64": "1.0.83-1", - "@github/copilot-linuxmusl-x64": "1.0.83-1", - "@github/copilot-win32-arm64": "1.0.83-1", - "@github/copilot-win32-x64": "1.0.83-1" + "@github/copilot-darwin-arm64": "1.0.83-2", + "@github/copilot-darwin-x64": "1.0.83-2", + "@github/copilot-linux-arm64": "1.0.83-2", + "@github/copilot-linux-x64": "1.0.83-2", + "@github/copilot-linuxmusl-arm64": "1.0.83-2", + "@github/copilot-linuxmusl-x64": "1.0.83-2", + "@github/copilot-win32-arm64": "1.0.83-2", + "@github/copilot-win32-x64": "1.0.83-2" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-1", - "integrity": "sha512-gxYJEd4yoIBIQHRmJw6ZegSkR6VuNm7AJVdAdARcU0SS68VZjtZFT29luThAp75Pxp37S5w+3A/jQNtxGyHizw==", + "version": "1.0.83-2", + "integrity": "sha512-RBjF/zTJe+gp9PsthYtIHF36+y3A3Zv3w/2FAa9nHF6ople1+lYoQ0OC1z6tWQSpBx3n/fSkNt0ebMmfhWlQyw==", "cpu": [ "arm64" ], @@ -694,8 +694,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-1", - "integrity": "sha512-PL63+Ib1RH+Bpu2UYJ9E8tmD4NZD/YlhD25F1Q9Rb86AdgLEoRYlxjNxXXF9P9dMmJ/u5zm0rUtWIRxv/FRbPg==", + "version": "1.0.83-2", + "integrity": "sha512-GzaarluCiHUA4yrYxWIFEklwPLTYjxGvLyYjI0wW5IPseT7GLFjN7AkDGRU4Ny/mxTj1MbUEsJAWVtiNy5EUHg==", "cpu": [ "x64" ], @@ -709,8 +709,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-1", - "integrity": "sha512-7c4RVEuXQu9zepAWoXwkizpESztUbWUR1hLPl+o+19L+D8fXm+5XQRH5E22Ia7P7/xHW3Ml2o2x5cXFq0k7/7w==", + "version": "1.0.83-2", + "integrity": "sha512-iGD5Pc7vnzrAjqTK/BrI8MDtMqXyxUe7P6u8TiGt+4DzKe3v2myT6eFQ+z/JJOHNpiu6LUpTDzwS8WttZqqJpA==", "cpu": [ "arm64" ], @@ -724,8 +724,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-1", - "integrity": "sha512-Be0zUVysLYV66pnOPhMxpShQ8Ox5V07qcsSOoNzG7iwpzkfZuf70U9CHk/fnI//mWwmZR+caPUfVM9BpwrdNnA==", + "version": "1.0.83-2", + "integrity": "sha512-jOQwz075vRWat+RJiMNtD+7vm4DQ83Dvw4iakpNkLfqWMPXlliVr/tqL+SffeSo87LnhOKobZOJOjELS27drdg==", "cpu": [ "x64" ], @@ -739,8 +739,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-1", - "integrity": "sha512-PZNP8vZZJU8yIc3dNjGUBUCFPNjqZs8FPZLDxvL6XdHM7Ep4cLBa+QdbRsw+nZONIaG0GtQf26eqk5ymYYpUPA==", + "version": "1.0.83-2", + "integrity": "sha512-6/+ByEBEV/vDofrCymaKrEZ5/p+TVHQfgWu8ywvWEPALfxj/iHpAF3grM2bj7uED4C19LTSarHNCkCqdrAUzIQ==", "cpu": [ "arm64" ], @@ -754,8 +754,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-1", - "integrity": "sha512-uKMhGtg5K07nA5/pxDSq8S5F/I+sMgVhUXg90Y4BI9trOidgxpXpCVyh4TeRNj2ENNugPvATdfGc6eFPsjmlgQ==", + "version": "1.0.83-2", + "integrity": "sha512-8dbiPUHaDemDibLfKXDe5xublJxt6fOht4yYDk/ikmOGAVezBUwRVO27PchXQBwGjP2PVNAASkv2WB4Ubw72lA==", "cpu": [ "x64" ], @@ -769,8 +769,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-1", - "integrity": "sha512-vMxHTmv3SotBuNI4LKcIFckd+n0OvhTik27qjsqOnVqFpvmHDghSsa8YPxySyxUQIE35z5pJaivVBK3dfEAY7w==", + "version": "1.0.83-2", + "integrity": "sha512-ZE1iUXlJSnNIH+VoRG96emy2e7jMJm85/pcKCrgLvqhDAcQSqiqLVv3OJp2VX2IcDxToOpiaGccU8Ab9zHQqkQ==", "cpu": [ "arm64" ], @@ -784,8 +784,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-1", - "integrity": "sha512-2l0VVTazW3/+p94ZQ5sAPCj1iH7KWxJAYh4yXWfvpsB/1JEEmyVgpLei1MeaMFVdtp/hi9bGprZ9+zWNlHh5hQ==", + "version": "1.0.83-2", + "integrity": "sha512-qfturLon+1oaWqSaXCISL6BEf5K+ijJFl0m/OZwstWYSlIlaE2+iOz5LcRukoxm25OG1JZDHQFp1k2PY2A+LbA==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index da1892a794..3c2bed310d 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-1", + "@github/copilot": "^1.0.83-2", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index bbb9cea89d..602facf807 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-1", + "@github/copilot": "^1.0.83-2", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 812c6b2607..eaa477ab0a 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, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; +import type { AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; /** A value that can be represented losslessly on the SDK JSON wire. */ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; @@ -869,6 +869,64 @@ export type DiscoveredExtensionMode = | "load_only" /** Extensions are loaded and the agent can create, reload, and manage them. */ | "load_and_augment"; +/** + * Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HookType". + */ +/** @experimental */ +export type HookType = + /** Runs before a tool is invoked. */ + | "preToolUse" + /** Runs before an MCP tool is invoked. */ + | "preMcpToolCall" + /** Runs after a tool completes successfully. */ + | "postToolUse" + /** Runs after a tool fails. */ + | "postToolUseFailure" + /** Runs after the user submits a prompt. */ + | "userPromptSubmitted" + /** Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. */ + | "userPromptTransformed" + /** Runs when a session starts. */ + | "sessionStart" + /** Runs when a session ends. */ + | "sessionEnd" + /** Runs after an agent result is produced. */ + | "postResult" + /** Runs before a pull request description is generated. */ + | "prePRDescription" + /** Runs when the agent encounters an error. */ + | "errorOccurred" + /** Runs when the agent stops. */ + | "agentStop" + /** Runs when a subagent starts. */ + | "subagentStart" + /** Runs when a subagent stops. */ + | "subagentStop" + /** Runs before conversation context is compacted. */ + | "preCompact" + /** Runs when the agent requests permission. */ + | "permissionRequest" + /** Runs when the agent emits a notification. */ + | "notification"; +/** + * Configuration tier that contributed a discovered hook action. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HookOrigin". + */ +/** @experimental */ +export type HookOrigin = + /** Hook loaded from user settings or the user's hook directory. */ + | "user" + /** Hook loaded from repository settings or the repository hook directory. */ + | "repository" + /** Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. */ + | "plugin" + /** Hook enforced by centrally managed policy. */ + | "policy"; /** * Server transport type: stdio, http, sse (deprecated), or memory * @@ -1348,49 +1406,6 @@ export type HistoryRewindOutcome = | "checkpoint-cleanup-failed" /** Files and conversation were rewound, but obsolete file snapshots could not be removed; only conversation-and-files rewinds produce this. */ | "snapshot-prune-failed"; -/** - * Hook event name dispatched through the SDK callback transport. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HookType". - */ -/** @experimental */ -/** @internal */ -export type HookType = - /** Runs before a tool is invoked. */ - | "preToolUse" - /** Runs before an MCP tool is invoked. */ - | "preMcpToolCall" - /** Runs after a tool completes successfully. */ - | "postToolUse" - /** Runs after a tool fails. */ - | "postToolUseFailure" - /** Runs after the user submits a prompt. */ - | "userPromptSubmitted" - /** Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. */ - | "userPromptTransformed" - /** Runs when a session starts. */ - | "sessionStart" - /** Runs when a session ends. */ - | "sessionEnd" - /** Runs after an agent result is produced. */ - | "postResult" - /** Runs before a pull request description is generated. */ - | "prePRDescription" - /** Runs when the agent encounters an error. */ - | "errorOccurred" - /** Runs when the agent stops. */ - | "agentStop" - /** Runs when a subagent starts. */ - | "subagentStart" - /** Runs when a subagent stops. */ - | "subagentStop" - /** Runs before conversation context is compacted. */ - | "preCompact" - /** Runs when the agent requests permission. */ - | "permissionRequest" - /** Runs when the agent emits a notification. */ - | "notification"; /** * Source for direct repo installs (when marketplace is empty) * @@ -4589,7 +4604,7 @@ export interface AgentGetCurrentResult { agent?: AgentInfo | null; } /** - * Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. + * Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "AgentInfo". @@ -4629,6 +4644,11 @@ export interface AgentInfo { * Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. */ model?: string; + /** + * Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + */ + models?: string[]; + modelPolicy?: AgentModelPolicy; /** * MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. * @@ -6841,6 +6861,37 @@ export interface DiscoveredExtensionsEnableRequest { */ ids: string[]; } +/** + * One server-discovered hook action from user, repository, plugin, or managed-policy configuration. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiscoveredHook". + */ +/** @experimental */ +export interface DiscoveredHook { + /** + * Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. + */ + id: string; + hookType: HookType; + origin: HookOrigin; + /** + * Human-readable source label, such as a hook file path, settings source, or plugin name. + */ + source?: string; + /** + * Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + */ + projectPath?: string; + /** + * Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + */ + enabled: boolean; + /** + * Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + */ + disableKey?: string; +} /** * MCP server discovered by `mcp.discover`, with config source, optional plugin source, transport type, and enabled state. * @@ -8982,6 +9033,44 @@ export interface HookInvokeRequest { export interface HookInvokeResponse { output?: JsonValue; } +/** + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HooksDiscoverRequest". + */ +/** @experimental */ +export interface HooksDiscoverRequest { + /** + * Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + */ + projectPaths?: string[]; + /** + * When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + */ + excludeHostHooks?: boolean; +} +/** + * Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HooksDiscoverResult". + */ +/** @experimental */ +export interface HooksDiscoverResult { + /** + * All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + */ + hooks: DiscoveredHook[]; + /** + * Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + */ + warnings: string[]; + /** + * Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + */ + errors: string[]; +} /** * Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. * @@ -15874,6 +15963,10 @@ export interface QueuePendingItems { * Stable opaque id for the canonical queued item. Batch rows share one id. */ id: string; + /** + * Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + */ + messageId?: string; kind: QueuePendingItemsKind; /** * Human-readable text to display for this queue entry in the UI @@ -16560,7 +16653,7 @@ export interface SandboxConfigUserPolicyNetwork { /** @experimental */ export interface SandboxConfigUserPolicyNetworkProxy { /** - * Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + * Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ url: string; /** @@ -19496,6 +19589,28 @@ export interface SessionsPruneOldRequest { */ excludeSessionIds?: string[]; } +/** + * Pagination options for reading an inactive or active local session's persisted event journal. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsReadPersistedEventsRequest". + */ +/** @experimental */ +export interface SessionsReadPersistedEventsRequest { + /** + * Session ID whose persisted event journal should be read. + */ + sessionId: string; + /** + * Opaque cursor returned by a previous persisted-event read. Omit on the first call. + */ + cursor?: string; + /** + * Maximum number of events to return in this batch (1–1000, default 200). + */ + max?: number; + direction?: EventsReadDirection; +} /** * Session ID whose in-use lock should be released. * @@ -20505,6 +20620,7 @@ export interface SubagentSettingsEntry { * Model override for matching subagents */ model?: string; + modelPolicy?: AgentModelPolicy; /** * Reasoning effort override for matching subagents */ @@ -22852,6 +22968,18 @@ export function createServerRpc(connection: MessageConnection) { ping: async (params: PingRequest): Promise => connection.sendRequest("ping", params), /** @experimental */ + hooks: { + /** + * Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + * + * @param params Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * @returns Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + */ + discover: async (params: HooksDiscoverRequest): Promise => + connection.sendRequest("hooks.discover", params), + }, + /** @experimental */ models: { /** * Lists Copilot models available to the authenticated user. @@ -23377,6 +23505,15 @@ export function createServerRpc(connection: MessageConnection) { */ list: async (params: SessionsListRequest): Promise => connection.sendRequest("sessions.list", params), + /** + * Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session. + * + * @param params Pagination options for reading an inactive or active local session's persisted event journal. + * + * @returns Batch of session events returned by a read, with cursor and continuation metadata. + */ + readPersistedEvents: async (params: SessionsReadPersistedEventsRequest): Promise => + connection.sendRequest("sessions.readPersistedEvents", params), /** * Finds the local session bound to a GitHub task ID, if any. * diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index a7049dd9fc..93576aabf5 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -41,6 +41,7 @@ export type SessionEvent = | CompactionStartEvent | CompactionCompleteEvent | TaskCompleteEvent + | CompletionReceiptEvent | FusionRouteStartedEvent | FusionRouteFailedEvent | FusionResolvedEvent @@ -50,6 +51,7 @@ export type SessionEvent = | AssistantTurnStartEvent | AssistantIntentEvent | AssistantFusionPhaseStartedEvent + | AssistantFusionPhaseActivityEvent | AssistantFusionPhaseCompletedEvent | AssistantFusionPhaseFailedEvent | AssistantServerToolProgressEvent @@ -317,6 +319,30 @@ export type TaskCompletionOutcome = | "continue" /** Completion cannot proceed without intervention; the active objective is paused when one is identified. */ | "blocked"; +/** + * Structured terminal status from a tool completion event. + */ +export type CompletionReceiptToolStatus = + /** The tool completed successfully. */ + | "success" + /** The tool failed without a more specific structured status. */ + | "failure" + /** The tool exceeded its time budget. */ + | "timeout" + /** The user rejected the tool call. */ + | "rejected" + /** The permissions service denied the tool call. */ + | "denied"; +/** + * Runtime reason the completion decision was accepted. + */ +export type CompletionReceiptStopReason = + /** The model reached a natural terminal response. */ + | "natural" + /** A terminal tool ended the interaction. */ + | "terminal_tool" + /** The configured agentStop continuation limit was reached. */ + | "agent_stop_block_limit"; /** * Kind of turn for which HydraFusion routing is running. */ @@ -346,6 +372,34 @@ export type FusionPattern = | "cascade" /** Run a primary draft, a read-only critique, and a revision. */ | "critique"; +/** + * HydraFusion phase kind. + */ +/** @experimental */ +export type FusionPhaseKind = + /** Primary solver phase. */ + | "primary" + /** Read-only cascade judge phase. */ + | "judge" + /** Cascade repair phase. */ + | "repair" + /** Initial critique-pattern draft phase. */ + | "draft" + /** Read-only critique phase. */ + | "critic" + /** Critique-pattern revision phase. */ + | "revision" + /** Follow-up phase continuing from the resolved model. */ + | "follow_up"; +/** + * Conversation scope in which a HydraFusion phase executes. + */ +/** @experimental */ +export type FusionConversationScope = + /** Canonical root conversation history. */ + | "root" + /** Isolated read-only review history that does not enter the root conversation. */ + | "review"; /** * The agent mode that was active when this message was sent */ @@ -406,33 +460,16 @@ export type UserMessageDelivery = /** Enqueued while the agent was busy; processed as its own run afterward. */ | "queued"; /** - * Conversation scope in which a HydraFusion phase executes. + * Content-safe activity observed while a HydraFusion phase is running. */ /** @experimental */ -export type FusionConversationScope = - /** Canonical root conversation history. */ - | "root" - /** Isolated read-only review history that does not enter the root conversation. */ - | "review"; -/** - * HydraFusion phase kind. - */ -/** @experimental */ -export type FusionPhaseKind = - /** Primary solver phase. */ - | "primary" - /** Read-only cascade judge phase. */ - | "judge" - /** Cascade repair phase. */ - | "repair" - /** Initial critique-pattern draft phase. */ - | "draft" - /** Read-only critique phase. */ - | "critic" - /** Critique-pattern revision phase. */ - | "revision" - /** Follow-up phase continuing from the resolved model. */ - | "follow_up"; +export type FusionPhaseActivityKind = + /** The provider produced additional private output bytes. */ + | "model_output" + /** A tool began executing inside the phase. */ + | "tool_started" + /** A tool finished executing inside the phase. */ + | "tool_completed"; /** * How a durable phase checkpoint contributes its exact message to canonical root history. */ @@ -1018,6 +1055,14 @@ export type SkillSource = | "custom" /** Skill bundled with the runtime. */ | "builtin"; +/** + * Whether configured models are advisory preferences or required constraints + */ +export type AgentModelPolicy = + /** Treat the authored models as advisory preferences that callers may override. */ + | "preferred" + /** Require subagent execution to use one of the authored models. */ + | "required"; /** * Configuration source: user, workspace, plugin, or builtin */ @@ -3068,6 +3113,97 @@ export interface TaskCompleteData { */ summary?: string; } +/** + * Session event "session.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. + */ +/** @experimental */ +export interface CompletionReceiptEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: CompletionReceiptData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * 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 "session.completion_receipt". + */ + type: "session.completion_receipt"; +} +/** + * Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. + */ +/** @experimental */ +export interface CompletionReceiptData { + /** + * One-based accepted completion receipt ordinal in the durable session history. + */ + attempt: number; + eventRange: CompletionReceiptEventRange; + /** + * Number of failed structured tool completions in the covered range. + */ + failedToolCount: number; + finalTool?: CompletionReceiptFinalTool; + /** + * Version of the completion receipt payload. + */ + schemaVersion: number; + /** + * Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + */ + sourceEventId: string; + stopReason: CompletionReceiptStopReason; + /** + * Number of successful structured tool completions in the covered range. + */ + successfulToolCount: number; +} +/** + * Inclusive durable event range summarized by a completion receipt. + */ +export interface CompletionReceiptEventRange { + /** + * Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + */ + endEventId: string; + /** + * Identifier of the user message that starts the covered exchange. + */ + startEventId: string; +} +/** + * Final structured tool completion in the covered event range. + */ +export interface CompletionReceiptFinalTool { + /** + * Process exit code from a structured shell result, when available. + */ + exitCode?: number; + status: CompletionReceiptToolStatus; + /** + * Unique identifier of the completed tool call. + */ + toolCallId: string; + /** + * Tool name from the matching tool execution start event, when available. + */ + toolName?: string; +} /** * Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. */ @@ -3241,6 +3377,12 @@ export interface FusionResolvedData { */ modelUniverseVersion?: string; pattern: FusionPattern; + /** + * Presentation-neutral phase plan for clients that render workflow progress. + * + * @experimental + */ + phasePlan?: FusionPhasePlanStep[]; /** * Version of the validated execution-plan format. */ @@ -3299,6 +3441,22 @@ export interface FusionFollowUpRecommendation { compactionTurn: FusionFollowUpAction; userTurn: FusionFollowUpAction; } +/** + * Presentation-neutral phase planned for a HydraFusion turn. + */ +/** @experimental */ +export interface FusionPhasePlanStep { + /** + * Whether the phase executes only when an earlier phase requests it. + */ + conditional: boolean; + kind: FusionPhaseKind; + /** + * Semantic role assigned to the phase. + */ + role: string; + scope: FusionConversationScope; +} /** * Validated HydraFusion routing capability scores. */ @@ -3479,6 +3637,10 @@ export interface UserMessageData { * 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. */ isAutopilotContinuation?: boolean; + /** + * Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots + */ + messageId?: string; /** * Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit */ @@ -4118,6 +4280,67 @@ export interface FusionPhaseStartedData { */ role: string; } +/** + * Session event "assistant.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase. + */ +/** @experimental */ +export interface AssistantFusionPhaseActivityEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionPhaseActivityData; + /** + * 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.fusion_phase_activity". + */ + type: "assistant.fusion_phase_activity"; +} +/** + * Experimental content-safe activity signal for a running HydraFusion phase. + */ +/** @experimental */ +export interface FusionPhaseActivityData { + activity: FusionPhaseActivityKind; + conversationScope: FusionConversationScope; + /** + * Identifier of the HydraFusion turn containing the phase. + */ + fusionId: string; + pattern: FusionPattern; + /** + * Stable identifier for the concrete phase. + */ + phaseId: string; + phaseKind: FusionPhaseKind; + /** + * Semantic role assigned to the phase. + */ + role: string; + /** + * Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + */ + toolCallId?: string; + /** + * Cumulative private response bytes observed for this model call. The event never includes response text. + */ + totalResponseSizeBytes?: number; +} /** * Session event "assistant.fusion_phase_completed". Experimental durable HydraFusion phase output and lossless replay checkpoint. */ @@ -6838,6 +7061,10 @@ export interface SubagentCompletedData { * Model used by the sub-agent */ model?: string; + /** + * Why an explicit task-call model did not become the effective model + */ + modelOverrideReason?: string; /** * Tool call ID of the parent tool invocation that spawned this sub-agent */ @@ -6925,6 +7152,10 @@ export interface SubagentFailedData { * Model selected for the sub-agent, when known */ model?: string; + /** + * Why an explicit task-call model did not become the effective model + */ + modelOverrideReason?: string; /** * Tool call ID of the parent tool invocation that spawned this sub-agent */ @@ -7589,6 +7820,7 @@ export interface PermissionRequestedEvent { * Permission request notification requiring client approval with request details */ export interface PermissionRequestedData { + agentMode?: SessionMode; permissionRequest: PermissionRequest; promptRequest?: PermissionPromptRequest; /** @@ -10798,7 +11030,7 @@ export interface CustomAgentsUpdatedData { warnings: string[]; } /** - * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. + * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. */ export interface CustomAgentsUpdatedAgent { /** @@ -10817,6 +11049,11 @@ export interface CustomAgentsUpdatedAgent { * Model override for this agent, if set */ model?: string; + modelPolicy?: AgentModelPolicy; + /** + * Authored model ids in priority order, if configured + */ + models?: string[]; /** * Internal name of the agent */ diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 7b629d5c18..b4edc24e61 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, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity +from .session_events import AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity if TYPE_CHECKING: from .._jsonrpc import JsonRpcClient @@ -2498,6 +2498,42 @@ def to_dict(self) -> dict: result["ids"] = from_list(from_str, self.ids) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class HookType(Enum): + """Hook event that invokes this action. + + Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally + support callback-only events. + """ + AGENT_STOP = "agentStop" + ERROR_OCCURRED = "errorOccurred" + NOTIFICATION = "notification" + PERMISSION_REQUEST = "permissionRequest" + POST_RESULT = "postResult" + POST_TOOL_USE = "postToolUse" + POST_TOOL_USE_FAILURE = "postToolUseFailure" + PRE_COMPACT = "preCompact" + PRE_MCP_TOOL_CALL = "preMcpToolCall" + PRE_PR_DESCRIPTION = "prePRDescription" + PRE_TOOL_USE = "preToolUse" + SESSION_END = "sessionEnd" + SESSION_START = "sessionStart" + SUBAGENT_START = "subagentStart" + SUBAGENT_STOP = "subagentStop" + USER_PROMPT_SUBMITTED = "userPromptSubmitted" + USER_PROMPT_TRANSFORMED = "userPromptTransformed" + +# Experimental: this type is part of an experimental API and may change or be removed. +class HookOrigin(Enum): + """Configuration tier that contributed this hook action. + + Configuration tier that contributed a discovered hook action. + """ + PLUGIN = "plugin" + POLICY = "policy" + REPOSITORY = "repository" + USER = "user" + # Experimental: this type is part of an experimental API and may change or be removed. class DiscoveredMCPServerType(Enum): """Server transport type: stdio, http, sse (deprecated), or memory""" @@ -2585,6 +2621,9 @@ class EventsReadDirection(Enum): cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. + + Direction to page through persisted history. Forward starts at the beginning; backward + starts with the newest events. Events in each page remain chronological. """ BACKWARD = "backward" FORWARD = "forward" @@ -4009,28 +4048,6 @@ def to_dict(self) -> dict: class HMACAuthInfoType(Enum): HMAC = "hmac" -# Internal: this type is an internal SDK API and is not part of the public surface. -class _HookType(Enum): - """Hook event name dispatched through the SDK callback transport.""" - - AGENT_STOP = "agentStop" - ERROR_OCCURRED = "errorOccurred" - NOTIFICATION = "notification" - PERMISSION_REQUEST = "permissionRequest" - POST_RESULT = "postResult" - POST_TOOL_USE = "postToolUse" - POST_TOOL_USE_FAILURE = "postToolUseFailure" - PRE_COMPACT = "preCompact" - PRE_MCP_TOOL_CALL = "preMcpToolCall" - PRE_PR_DESCRIPTION = "prePRDescription" - PRE_TOOL_USE = "preToolUse" - SESSION_END = "sessionEnd" - SESSION_START = "sessionStart" - SUBAGENT_START = "subagentStart" - SUBAGENT_STOP = "subagentStop" - USER_PROMPT_SUBMITTED = "userPromptSubmitted" - USER_PROMPT_TRANSFORMED = "userPromptTransformed" - # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass class _HookInvokeResponse: @@ -4050,6 +4067,38 @@ def to_dict(self) -> dict: result["output"] = self.output return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HooksDiscoverRequest: + """Optional project paths and host-exclusion behavior for server-scoped hook discovery.""" + + exclude_host_hooks: bool | None = None + """When true, omit host-owned user and plugin hook rows and their diagnostics. + Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks + still contribute to each remaining row's effective enabled state. This filters sources + rather than simulating a host with no settings. + """ + project_paths: list[str] | None = None + """Optional project directory paths whose trusted repository and project-expanded plugin + hooks should be discovered. When omitted or empty, user, managed-policy, and globally + enabled installed or explicit plugin hooks are returned without project expansion. + """ + + @staticmethod + def from_dict(obj: Any) -> 'HooksDiscoverRequest': + assert isinstance(obj, dict) + exclude_host_hooks = from_union([from_bool, from_none], obj.get("excludeHostHooks")) + project_paths = from_union([lambda x: from_list(from_str, x), from_none], obj.get("projectPaths")) + return HooksDiscoverRequest(exclude_host_hooks, project_paths) + + def to_dict(self) -> dict: + result: dict = {} + if self.exclude_host_hooks is not None: + result["excludeHostHooks"] = from_union([from_bool, from_none], self.exclude_host_hooks) + if self.project_paths is not None: + result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths) + return result + class InstalledPluginSourceURLSource(Enum): GITHUB = "github" LOCAL = "local" @@ -9608,22 +9657,25 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SandboxConfigUserPolicyNetworkProxy: - """HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and - cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. - Credentials go in the separate `username`/`password` fields. A credential-free http:// - loopback proxy URL is routed through the localhost proxy automatically; an https:// or - authenticated loopback URL is used as-is. + """HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, + requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is + accepted and routed through the IPv4 gateway), and does not support proxy credentials. + macOS relies on applications honoring proxy environment variables. Windows also + configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's + networking stack. Configure supported credentials in the separate `username` and + `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, + while an https:// or authenticated loopback URL uses the URL form. HTTP proxy configuration for sandboxed traffic. """ url: str """Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the - scheme's standard port when omitted. Credentials must not be embedded here — a - `user:pass@` authority is rejected; put them in the separate `username`/`password` - fields. A credential-free http:// loopback URL is routed through the localhost proxy - automatically; loopback covers localhost and any *.localhost subdomain, the whole - 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or - one with a username/password set, is used as-is. + scheme's standard port when omitted; an explicit port must be between 1 and 65535. + Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in + the separate `username`/`password` fields. A credential-free http:// loopback proxy URL + is routed through the localhost proxy automatically; loopback covers localhost and any + *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback + (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. """ password: str | None = None """Optional password for proxy authentication, combined with the URL at spawn time. The @@ -16327,6 +16379,132 @@ def to_dict(self) -> dict: result["plugin"] = from_union([lambda x: to_class(DiscoveredExtensionPlugin, x), from_none], self.plugin) return result +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _HookInvokeRequest: + """Runtime-owned wire payload for a server-to-client hook callback invocation.""" + + hook_type: HookType + input: Any + session_id: str + + @staticmethod + def from_dict(obj: Any) -> '_HookInvokeRequest': + assert isinstance(obj, dict) + hook_type = HookType(obj.get("hookType")) + input = obj.get("input") + session_id = from_str(obj.get("sessionId")) + return _HookInvokeRequest(hook_type, input, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["hookType"] = to_enum(HookType, self.hook_type) + result["input"] = self.input + result["sessionId"] = from_str(self.session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class DiscoveredHook: + """One server-discovered hook action from user, repository, plugin, or managed-policy + configuration. + """ + enabled: bool + """Whether this action is enabled under the server-side discovery settings. Concrete + sessions may differ because they can add session-specific directories, plugins, or trust. + False when its disable key is present in the user's disabled-hooks setting or disable-all + settings suppress the action. + """ + hook_type: HookType + """Hook event that invokes this action.""" + + id: str + """Deterministic identifier for this server-discovered action row. It remains stable while + the project, origin, source, event, action content, and duplicate ordinal are unchanged. + This is row identity, not the key persisted in disabledHooks. + """ + origin: HookOrigin + """Configuration tier that contributed this hook action.""" + + disable_key: str | None = None + """Durable content hash used by hook enablement. Identical actions may intentionally share + this key. Omitted when changing the user's disabled-hooks setting cannot change the + action's current server-discovered state, including managed-policy hooks, session-start + prompt actions, actions suppressed by disable-all settings, and projectless plugin + actions that require project-directory expansion. + """ + project_path: str | None = None + """Input project path for which this server-side action was resolved. Set on every row + returned for project-scoped discovery, including repeated user and policy actions. + """ + source: str | None = None + """Human-readable source label, such as a hook file path, settings source, or plugin name.""" + + @staticmethod + def from_dict(obj: Any) -> 'DiscoveredHook': + assert isinstance(obj, dict) + enabled = from_bool(obj.get("enabled")) + hook_type = HookType(obj.get("hookType")) + id = from_str(obj.get("id")) + origin = HookOrigin(obj.get("origin")) + disable_key = from_union([from_str, from_none], obj.get("disableKey")) + project_path = from_union([from_str, from_none], obj.get("projectPath")) + source = from_union([from_str, from_none], obj.get("source")) + return DiscoveredHook(enabled, hook_type, id, origin, disable_key, project_path, source) + + def to_dict(self) -> dict: + result: dict = {} + result["enabled"] = from_bool(self.enabled) + result["hookType"] = to_enum(HookType, self.hook_type) + result["id"] = from_str(self.id) + result["origin"] = to_enum(HookOrigin, self.origin) + if self.disable_key is not None: + result["disableKey"] = from_union([from_str, from_none], self.disable_key) + if self.project_path is not None: + result["projectPath"] = from_union([from_str, from_none], self.project_path) + if self.source is not None: + result["source"] = from_union([from_str, from_none], self.source) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionsReadPersistedEventsRequest: + """Pagination options for reading an inactive or active local session's persisted event + journal. + """ + session_id: str + """Session ID whose persisted event journal should be read.""" + + cursor: str | None = None + """Opaque cursor returned by a previous persisted-event read. Omit on the first call.""" + + direction: EventsReadDirection | None = None + """Direction to page through persisted history. Forward starts at the beginning; backward + starts with the newest events. Events in each page remain chronological. + """ + max: int | None = None + """Maximum number of events to return in this batch (1–1000, default 200).""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionsReadPersistedEventsRequest': + assert isinstance(obj, dict) + session_id = from_str(obj.get("sessionId")) + cursor = from_union([from_str, from_none], obj.get("cursor")) + direction = from_union([EventsReadDirection, from_none], obj.get("direction")) + max = from_union([from_int, from_none], obj.get("max")) + return SessionsReadPersistedEventsRequest(session_id, cursor, direction, max) + + def to_dict(self) -> dict: + result: dict = {} + result["sessionId"] = from_str(self.session_id) + if self.cursor is not None: + result["cursor"] = from_union([from_str, from_none], self.cursor) + if self.direction is not None: + result["direction"] = from_union([lambda x: to_enum(EventsReadDirection, x), from_none], self.direction) + if self.max is not None: + result["max"] = from_union([from_int, from_none], self.max) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class EventLogReadRequest: @@ -17605,30 +17783,6 @@ def to_dict(self) -> dict: result["mode"] = to_enum(HistoryRewindMode, self.mode) return result -# Internal: this type is an internal SDK API and is not part of the public surface. -@dataclass -class _HookInvokeRequest: - """Runtime-owned wire payload for a server-to-client hook callback invocation.""" - - hook_type: _HookType - input: Any - session_id: str - - @staticmethod - def from_dict(obj: Any) -> '_HookInvokeRequest': - assert isinstance(obj, dict) - hook_type = _HookType(obj.get("hookType")) - input = obj.get("input") - session_id = from_str(obj.get("sessionId")) - return _HookInvokeRequest(hook_type, input, session_id) - - def to_dict(self) -> dict: - result: dict = {} - result["hookType"] = to_enum(_HookType, self.hook_type) - result["input"] = self.input - result["sessionId"] = from_str(self.session_id) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class InstalledPluginSource: @@ -22385,6 +22539,11 @@ class QueuePendingItems: kind: QueuePendingItemsKind """Whether this item is a queued user message or a queued slash command / model change""" + message_id: str | None = None + """Stable identity of the queued user message. Present for message rows and absent for slash + commands and model changes. + """ + @staticmethod def from_dict(obj: Any) -> 'QueuePendingItems': assert isinstance(obj, dict) @@ -22392,7 +22551,8 @@ def from_dict(obj: Any) -> 'QueuePendingItems': display_text = from_str(obj.get("displayText")) id = from_str(obj.get("id")) kind = QueuePendingItemsKind(obj.get("kind")) - return QueuePendingItems(agent_mode, display_text, id, kind) + message_id = from_union([from_str, from_none], obj.get("messageId")) + return QueuePendingItems(agent_mode, display_text, id, kind, message_id) def to_dict(self) -> dict: result: dict = {} @@ -22400,6 +22560,8 @@ def to_dict(self) -> dict: result["displayText"] = from_str(self.display_text) result["id"] = from_str(self.id) result["kind"] = to_enum(QueuePendingItemsKind, self.kind) + if self.message_id is not None: + result["messageId"] = from_union([from_str, from_none], self.message_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -22662,11 +22824,14 @@ class SandboxConfigUserPolicyNetwork: """Whether outbound network traffic is allowed at all.""" proxy: SandboxConfigUserPolicyNetworkProxy | None = None - """HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and - cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. - Credentials go in the separate `username`/`password` fields. A credential-free http:// - loopback proxy URL is routed through the localhost proxy automatically; an https:// or - authenticated loopback URL is used as-is. + """HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, + requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is + accepted and routed through the IPv4 gateway), and does not support proxy credentials. + macOS relies on applications honoring proxy environment variables. Windows also + configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's + networking stack. Configure supported credentials in the separate `username` and + `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, + while an https:// or authenticated loopback URL uses the URL form. """ @staticmethod @@ -23469,7 +23634,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AgentInfo: - """Agent metadata, including identifiers, display details, source, tools, model, MCP + """Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. The newly selected custom agent @@ -23496,6 +23661,13 @@ class AgentInfo: """Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. """ + model_policy: AgentModelPolicy | None = None + """Whether authored models are preferences or required constraints.""" + + models: list[str] | None = None + """Authored preferred model ids for this agent, in priority order. Runtime model selection + chooses the first available model; omitted means no authored preference. + """ path: str | None = None """Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. @@ -23527,13 +23699,15 @@ def from_dict(obj: Any) -> 'AgentInfo': name = from_str(obj.get("name")) mcp_servers = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("mcpServers")) model = from_union([from_str, from_none], obj.get("model")) + model_policy = from_union([AgentModelPolicy, from_none], obj.get("modelPolicy")) + models = from_union([lambda x: from_list(from_str, x), from_none], obj.get("models")) path = from_union([from_str, from_none], obj.get("path")) prompt = from_union([from_str, from_none], obj.get("prompt")) skills = from_union([lambda x: from_list(from_str, x), from_none], obj.get("skills")) source = from_union([AgentInfoSource, from_none], obj.get("source")) tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tools")) user_invocable = from_union([from_bool, from_none], obj.get("userInvocable")) - return AgentInfo(description, display_name, id, name, mcp_servers, model, path, prompt, skills, source, tools, user_invocable) + return AgentInfo(description, display_name, id, name, mcp_servers, model, model_policy, models, path, prompt, skills, source, tools, user_invocable) def to_dict(self) -> dict: result: dict = {} @@ -23545,6 +23719,10 @@ def to_dict(self) -> dict: result["mcpServers"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.mcp_servers) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) + if self.model_policy is not None: + result["modelPolicy"] = from_union([lambda x: to_enum(AgentModelPolicy, x), from_none], self.model_policy) + if self.models is not None: + result["models"] = from_union([lambda x: from_list(from_str, x), from_none], self.models) if self.path is not None: result["path"] = from_union([from_str, from_none], self.path) if self.prompt is not None: @@ -25606,6 +25784,43 @@ def to_dict(self) -> dict: result["mode"] = to_enum(DiscoveredExtensionMode, self.mode) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HooksDiscoverResult: + """Server-discovered hook actions and partial-load diagnostics from user, repository, + plugin, and managed-policy sources. Concrete sessions may include additional + session-specific hook sources. + """ + errors: list[str] + """Errors for hook sources or actions that could not be loaded, making the result partially + incomplete. Other valid actions are still returned. Project-resolution and + repository-settings errors are prefixed with their project path. + """ + hooks: list[DiscoveredHook] + """All discovered hook actions. Byte-identical actions remain separate rows even when they + share a disable key. + """ + warnings: list[str] + """Non-fatal source-loading warnings. Discovery remains complete for the affected source, + although the source had a recoverable issue. Repository-settings warnings are prefixed + with their project path when attribution is available. + """ + + @staticmethod + def from_dict(obj: Any) -> 'HooksDiscoverResult': + assert isinstance(obj, dict) + errors = from_list(from_str, obj.get("errors")) + hooks = from_list(DiscoveredHook.from_dict, obj.get("hooks")) + warnings = from_list(from_str, obj.get("warnings")) + return HooksDiscoverResult(errors, hooks, warnings) + + def to_dict(self) -> dict: + result: dict = {} + result["errors"] = from_list(from_str, self.errors) + result["hooks"] = from_list(lambda x: to_class(DiscoveredHook, x), self.hooks) + result["warnings"] = from_list(from_str, self.warnings) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ExtensionList: @@ -34132,8 +34347,8 @@ class ModelSwitchToRequest: """When true, evaluate context-window compaction policy before applying the switch.""" source: ModelChangeSource | None = None - """Origin to record on the effective `session.model_change` event. Defaults to `sdk` when - omitted. + """Origin to record on the effective `session.model_change` event for trusted in-process + calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. """ verbosity: Verbosity | None = None """Output verbosity level to request for supported models""" @@ -34858,13 +35073,17 @@ class SubagentSettingsEntry: model: str | None = None """Model override for matching subagents""" + model_policy: AgentModelPolicy | None = None + """Whether the configured model strategy is preferred or required""" + @staticmethod def from_dict(obj: Any) -> 'SubagentSettingsEntry': assert isinstance(obj, dict) context_tier = from_union([SubagentSettingsEntryContextTier, from_none], obj.get("contextTier")) effort_level = from_union([from_str, from_none], obj.get("effortLevel")) model = from_union([from_str, from_none], obj.get("model")) - return SubagentSettingsEntry(context_tier, effort_level, model) + model_policy = from_union([AgentModelPolicy, from_none], obj.get("modelPolicy")) + return SubagentSettingsEntry(context_tier, effort_level, model, model_policy) def to_dict(self) -> dict: result: dict = {} @@ -34874,6 +35093,8 @@ def to_dict(self) -> dict: result["effortLevel"] = from_union([from_str, from_none], self.effort_level) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) + if self.model_policy is not None: + result["modelPolicy"] = from_union([lambda x: to_enum(AgentModelPolicy, x), from_none], self.model_policy) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -35290,6 +35511,7 @@ class RPC: discovered_extensions_disable_request: DiscoveredExtensionsDisableRequest discovered_extensions_enable_request: DiscoveredExtensionsEnableRequest discovered_extension_source: DiscoveredExtensionSource + discovered_hook: DiscoveredHook discovered_mcp_server: DiscoveredMCPServer discovered_mcp_server_type: DiscoveredMCPServerType enqueue_command_params: EnqueueCommandParams @@ -35412,7 +35634,10 @@ class RPC: hmac_auth_info: HMACAuthInfo hook_invoke_request: _HookInvokeRequest hook_invoke_response: _HookInvokeResponse - hook_type: _HookType + hook_origin: HookOrigin + hooks_discover_request: HooksDiscoverRequest + hooks_discover_result: HooksDiscoverResult + hook_type: HookType installed_plugin: InstalledPlugin installed_plugin_info: InstalledPluginInfo installed_plugin_source: InstalledPluginSource | str @@ -36085,6 +36310,7 @@ class RPC: sessions_open_status: SessionsOpenStatus session_source: SessionSource sessions_prune_old_request: SessionsPruneOldRequest + sessions_read_persisted_events_request: SessionsReadPersistedEventsRequest sessions_register_extension_tools_on_session_options: SessionsRegisterExtensionToolsOnSessionOptions sessions_release_lock_request: SessionsReleaseLockRequest sessions_release_lock_result: SessionsReleaseLockResult @@ -36474,6 +36700,7 @@ def from_dict(obj: Any) -> 'RPC': discovered_extensions_disable_request = DiscoveredExtensionsDisableRequest.from_dict(obj.get("DiscoveredExtensionsDisableRequest")) discovered_extensions_enable_request = DiscoveredExtensionsEnableRequest.from_dict(obj.get("DiscoveredExtensionsEnableRequest")) discovered_extension_source = DiscoveredExtensionSource(obj.get("DiscoveredExtensionSource")) + discovered_hook = DiscoveredHook.from_dict(obj.get("DiscoveredHook")) discovered_mcp_server = DiscoveredMCPServer.from_dict(obj.get("DiscoveredMcpServer")) discovered_mcp_server_type = DiscoveredMCPServerType(obj.get("DiscoveredMcpServerType")) enqueue_command_params = EnqueueCommandParams.from_dict(obj.get("EnqueueCommandParams")) @@ -36596,7 +36823,10 @@ def from_dict(obj: Any) -> 'RPC': hmac_auth_info = HMACAuthInfo.from_dict(obj.get("HMACAuthInfo")) hook_invoke_request = _HookInvokeRequest.from_dict(obj.get("HookInvokeRequest")) hook_invoke_response = _HookInvokeResponse.from_dict(obj.get("HookInvokeResponse")) - hook_type = _HookType(obj.get("HookType")) + hook_origin = HookOrigin(obj.get("HookOrigin")) + hooks_discover_request = HooksDiscoverRequest.from_dict(obj.get("HooksDiscoverRequest")) + hooks_discover_result = HooksDiscoverResult.from_dict(obj.get("HooksDiscoverResult")) + hook_type = HookType(obj.get("HookType")) installed_plugin = InstalledPlugin.from_dict(obj.get("InstalledPlugin")) installed_plugin_info = InstalledPluginInfo.from_dict(obj.get("InstalledPluginInfo")) installed_plugin_source = from_union([InstalledPluginSource.from_dict, from_str], obj.get("InstalledPluginSource")) @@ -37269,6 +37499,7 @@ def from_dict(obj: Any) -> 'RPC': sessions_open_status = SessionsOpenStatus(obj.get("SessionsOpenStatus")) session_source = SessionSource(obj.get("SessionSource")) sessions_prune_old_request = SessionsPruneOldRequest.from_dict(obj.get("SessionsPruneOldRequest")) + sessions_read_persisted_events_request = SessionsReadPersistedEventsRequest.from_dict(obj.get("SessionsReadPersistedEventsRequest")) sessions_register_extension_tools_on_session_options = SessionsRegisterExtensionToolsOnSessionOptions.from_dict(obj.get("SessionsRegisterExtensionToolsOnSessionOptions")) sessions_release_lock_request = SessionsReleaseLockRequest.from_dict(obj.get("SessionsReleaseLockRequest")) sessions_release_lock_result = SessionsReleaseLockResult.from_dict(obj.get("SessionsReleaseLockResult")) @@ -37476,7 +37707,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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, 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, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, 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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_prune_old_request, sessions_read_persisted_events_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, 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, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -37658,6 +37889,7 @@ def to_dict(self) -> dict: result["DiscoveredExtensionsDisableRequest"] = to_class(DiscoveredExtensionsDisableRequest, self.discovered_extensions_disable_request) result["DiscoveredExtensionsEnableRequest"] = to_class(DiscoveredExtensionsEnableRequest, self.discovered_extensions_enable_request) result["DiscoveredExtensionSource"] = to_enum(DiscoveredExtensionSource, self.discovered_extension_source) + result["DiscoveredHook"] = to_class(DiscoveredHook, self.discovered_hook) result["DiscoveredMcpServer"] = to_class(DiscoveredMCPServer, self.discovered_mcp_server) result["DiscoveredMcpServerType"] = to_enum(DiscoveredMCPServerType, self.discovered_mcp_server_type) result["EnqueueCommandParams"] = to_class(EnqueueCommandParams, self.enqueue_command_params) @@ -37780,7 +38012,10 @@ def to_dict(self) -> dict: result["HMACAuthInfo"] = to_class(HMACAuthInfo, self.hmac_auth_info) result["HookInvokeRequest"] = to_class(_HookInvokeRequest, self.hook_invoke_request) result["HookInvokeResponse"] = to_class(_HookInvokeResponse, self.hook_invoke_response) - result["HookType"] = to_enum(_HookType, self.hook_type) + result["HookOrigin"] = to_enum(HookOrigin, self.hook_origin) + result["HooksDiscoverRequest"] = to_class(HooksDiscoverRequest, self.hooks_discover_request) + result["HooksDiscoverResult"] = to_class(HooksDiscoverResult, self.hooks_discover_result) + result["HookType"] = to_enum(HookType, self.hook_type) result["InstalledPlugin"] = to_class(InstalledPlugin, self.installed_plugin) result["InstalledPluginInfo"] = to_class(InstalledPluginInfo, self.installed_plugin_info) result["InstalledPluginSource"] = from_union([lambda x: to_class(InstalledPluginSource, x), from_str], self.installed_plugin_source) @@ -38453,6 +38688,7 @@ def to_dict(self) -> dict: result["SessionsOpenStatus"] = to_enum(SessionsOpenStatus, self.sessions_open_status) result["SessionSource"] = to_enum(SessionSource, self.session_source) result["SessionsPruneOldRequest"] = to_class(SessionsPruneOldRequest, self.sessions_prune_old_request) + result["SessionsReadPersistedEventsRequest"] = to_class(SessionsReadPersistedEventsRequest, self.sessions_read_persisted_events_request) result["SessionsRegisterExtensionToolsOnSessionOptions"] = to_class(SessionsRegisterExtensionToolsOnSessionOptions, self.sessions_register_extension_tools_on_session_options) result["SessionsReleaseLockRequest"] = to_class(SessionsReleaseLockRequest, self.sessions_release_lock_request) result["SessionsReleaseLockResult"] = to_class(SessionsReleaseLockResult, self.sessions_release_lock_result) @@ -39121,6 +39357,17 @@ def _patch_model_capabilities(data: dict) -> dict: return data +# Experimental: this API group is experimental and may change or be removed. +class ServerHooksApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def discover(self, params: HooksDiscoverRequest, *, timeout: float | None = None) -> HooksDiscoverResult: + "Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources.\n\nArgs:\n params: Optional project paths and host-exclusion behavior for server-scoped hook discovery.\n\nReturns:\n Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return HooksDiscoverResult.from_dict(await self._client.request("hooks.discover", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerModelsApi: def __init__(self, client: "JsonRpcClient"): @@ -39533,6 +39780,11 @@ async def list(self, params: SessionsListRequest, *, timeout: float | None = Non params_dict = {k: v for k, v in params.to_dict().items() if v is not None} return SessionList.from_dict(await self._client.request("sessions.list", params_dict, **_timeout_kwargs(timeout))) + async def read_persisted_events(self, params: SessionsReadPersistedEventsRequest, *, timeout: float | None = None) -> EventsReadResult: + "Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session.\n\nArgs:\n params: Pagination options for reading an inactive or active local session's persisted event journal.\n\nReturns:\n Batch of session events returned by a read, with cursor and continuation metadata." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return EventsReadResult.from_dict(await self._client.request("sessions.readPersistedEvents", params_dict, **_timeout_kwargs(timeout))) + async def find_by_task_id(self, params: SessionsFindByTaskIDRequest, *, timeout: float | None = None) -> SessionsFindByTaskIDResult: "Finds the local session bound to a GitHub task ID, if any.\n\nArgs:\n params: GitHub task ID to look up.\n\nReturns:\n ID of the local session bound to the given GitHub task, or omitted when none." params_dict = {k: v for k, v in params.to_dict().items() if v is not None} @@ -39642,6 +39894,7 @@ class ServerRpc: """Typed server-scoped RPC methods.""" def __init__(self, client: "JsonRpcClient"): self._client = client + self.hooks = ServerHooksApi(client) self.models = ServerModelsApi(client) self.tools = ServerToolsApi(client) self.account = ServerAccountApi(client) @@ -42109,6 +42362,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "DiscoveredExtensions", "DiscoveredExtensionsDisableRequest", "DiscoveredExtensionsEnableRequest", + "DiscoveredHook", "DiscoveredMCPServer", "DiscoveredMCPServerType", "EnqueueCommandParams", @@ -42252,6 +42506,10 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "HistorySummarizeForHandoffResult", "HistoryTruncateRequest", "HistoryTruncateResult", + "HookOrigin", + "HookType", + "HooksDiscoverRequest", + "HooksDiscoverResult", "HooksHandler", "Host", "HostType", @@ -42886,6 +43144,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ServerCatalogApi", "ServerCommandsApi", "ServerExtensionsApi", + "ServerHooksApi", "ServerInstructionSourceList", "ServerInstructionsApi", "ServerLlmInferenceApi", @@ -43071,6 +43330,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionsOpenResumeLastKind", "SessionsOpenStatus", "SessionsPruneOldRequest", + "SessionsReadPersistedEventsRequest", "SessionsRegisterExtensionToolsOnSessionOptions", "SessionsReleaseLockRequest", "SessionsReleaseLockResult", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 20b3df4be4..e3727eb24e 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -156,6 +156,8 @@ class SessionEventType(Enum): SESSION_COMPACTION_COMPLETE = "session.compaction_complete" SESSION_TASK_COMPLETE = "session.task_complete" # Experimental: this event is part of an experimental API and may change or be removed. + SESSION_COMPLETION_RECEIPT = "session.completion_receipt" + # Experimental: this event is part of an experimental API and may change or be removed. SESSION_FUSION_ROUTE_STARTED = "session.fusion_route_started" # Experimental: this event is part of an experimental API and may change or be removed. SESSION_FUSION_ROUTE_FAILED = "session.fusion_route_failed" @@ -172,6 +174,8 @@ class SessionEventType(Enum): # Experimental: this event is part of an experimental API and may change or be removed. ASSISTANT_FUSION_PHASE_STARTED = "assistant.fusion_phase_started" # Experimental: this event is part of an experimental API and may change or be removed. + ASSISTANT_FUSION_PHASE_ACTIVITY = "assistant.fusion_phase_activity" + # Experimental: this event is part of an experimental API and may change or be removed. ASSISTANT_FUSION_PHASE_COMPLETED = "assistant.fusion_phase_completed" # Experimental: this event is part of an experimental API and may change or be removed. ASSISTANT_FUSION_PHASE_FAILED = "assistant.fusion_phase_failed" @@ -398,6 +402,60 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AssistantFusionPhaseActivityData: + "Experimental content-safe activity signal for a running HydraFusion phase." + activity: FusionPhaseActivityKind + conversation_scope: FusionConversationScope + fusion_id: str + pattern: FusionPattern + phase_id: str + phase_kind: FusionPhaseKind + role: str + tool_call_id: str | None = None + total_response_size_bytes: int | None = None + + @staticmethod + def from_dict(obj: Any) -> "AssistantFusionPhaseActivityData": + assert isinstance(obj, dict) + activity = parse_enum(FusionPhaseActivityKind, obj.get("activity")) + conversation_scope = parse_enum(FusionConversationScope, obj.get("conversationScope")) + fusion_id = from_str(obj.get("fusionId")) + pattern = parse_enum(FusionPattern, obj.get("pattern")) + phase_id = from_str(obj.get("phaseId")) + phase_kind = parse_enum(FusionPhaseKind, obj.get("phaseKind")) + role = from_str(obj.get("role")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + total_response_size_bytes = from_union([from_none, from_int], obj.get("totalResponseSizeBytes")) + return AssistantFusionPhaseActivityData( + activity=activity, + conversation_scope=conversation_scope, + fusion_id=fusion_id, + pattern=pattern, + phase_id=phase_id, + phase_kind=phase_kind, + role=role, + tool_call_id=tool_call_id, + total_response_size_bytes=total_response_size_bytes, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["activity"] = to_enum(FusionPhaseActivityKind, self.activity) + result["conversationScope"] = to_enum(FusionConversationScope, self.conversation_scope) + result["fusionId"] = from_str(self.fusion_id) + result["pattern"] = to_enum(FusionPattern, self.pattern) + result["phaseId"] = from_str(self.phase_id) + result["phaseKind"] = to_enum(FusionPhaseKind, self.phase_kind) + result["role"] = from_str(self.role) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + if self.total_response_size_bytes is not None: + result["totalResponseSizeBytes"] = from_union([from_none, to_int], self.total_response_size_bytes) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AssistantFusionPhaseCompletedData: @@ -1202,6 +1260,38 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class FusionPhasePlanStep: + "Presentation-neutral phase planned for a HydraFusion turn." + conditional: bool + kind: FusionPhaseKind + role: str + scope: FusionConversationScope + + @staticmethod + def from_dict(obj: Any) -> "FusionPhasePlanStep": + assert isinstance(obj, dict) + conditional = from_bool(obj.get("conditional")) + kind = parse_enum(FusionPhaseKind, obj.get("kind")) + role = from_str(obj.get("role")) + scope = parse_enum(FusionConversationScope, obj.get("scope")) + return FusionPhasePlanStep( + conditional=conditional, + kind=kind, + role=role, + scope=scope, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["conditional"] = from_bool(self.conditional) + result["kind"] = to_enum(FusionPhaseKind, self.kind) + result["role"] = from_str(self.role) + result["scope"] = to_enum(FusionConversationScope, self.scope) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class FusionPhaseUsage: @@ -1678,6 +1768,55 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionCompletionReceiptData: + "Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted." + attempt: int + event_range: CompletionReceiptEventRange + failed_tool_count: int + schema_version: int + source_event_id: str + stop_reason: CompletionReceiptStopReason + successful_tool_count: int + final_tool: CompletionReceiptFinalTool | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionCompletionReceiptData": + assert isinstance(obj, dict) + attempt = from_int(obj.get("attempt")) + event_range = CompletionReceiptEventRange.from_dict(obj.get("eventRange")) + failed_tool_count = from_int(obj.get("failedToolCount")) + schema_version = from_int(obj.get("schemaVersion")) + source_event_id = from_str(obj.get("sourceEventId")) + stop_reason = parse_enum(CompletionReceiptStopReason, obj.get("stopReason")) + successful_tool_count = from_int(obj.get("successfulToolCount")) + final_tool = from_union([from_none, CompletionReceiptFinalTool.from_dict], obj.get("finalTool")) + return SessionCompletionReceiptData( + attempt=attempt, + event_range=event_range, + failed_tool_count=failed_tool_count, + schema_version=schema_version, + source_event_id=source_event_id, + stop_reason=stop_reason, + successful_tool_count=successful_tool_count, + final_tool=final_tool, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["attempt"] = to_int(self.attempt) + result["eventRange"] = to_class(CompletionReceiptEventRange, self.event_range) + result["failedToolCount"] = to_int(self.failed_tool_count) + result["schemaVersion"] = to_int(self.schema_version) + result["sourceEventId"] = from_str(self.source_event_id) + result["stopReason"] = to_enum(CompletionReceiptStopReason, self.stop_reason) + result["successfulToolCount"] = to_int(self.successful_tool_count) + if self.final_tool is not None: + result["finalTool"] = from_union([from_none, lambda x: to_class(CompletionReceiptFinalTool, x)], self.final_tool) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFusionCompletedData: @@ -1783,6 +1922,8 @@ class SessionFusionResolvedData: turn_id: str follow_up: FusionFollowUpRecommendation | None = None model_universe_version: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + phase_plan: list[FusionPhasePlanStep] | None = None plan_version: str | None = None policy_version: str | None = None route_source: str | None = None @@ -1807,6 +1948,7 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData": turn_id = from_str(obj.get("turnId")) follow_up = from_union([from_none, FusionFollowUpRecommendation.from_dict], obj.get("followUp")) model_universe_version = from_union([from_none, from_str], obj.get("modelUniverseVersion")) + phase_plan = from_union([from_none, lambda x: from_list(FusionPhasePlanStep.from_dict, x)], obj.get("phasePlan")) plan_version = from_union([from_none, from_str], obj.get("planVersion")) policy_version = from_union([from_none, from_str], obj.get("policyVersion")) route_source = from_union([from_none, from_str], obj.get("routeSource")) @@ -1828,6 +1970,7 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData": turn_id=turn_id, follow_up=follow_up, model_universe_version=model_universe_version, + phase_plan=phase_plan, plan_version=plan_version, policy_version=policy_version, route_source=route_source, @@ -1854,6 +1997,8 @@ def to_dict(self) -> dict: result["followUp"] = from_union([from_none, lambda x: to_class(FusionFollowUpRecommendation, x)], self.follow_up) if self.model_universe_version is not None: result["modelUniverseVersion"] = from_union([from_none, from_str], self.model_universe_version) + if self.phase_plan is not None: + result["phasePlan"] = from_union([from_none, lambda x: from_list(lambda x: to_class(FusionPhasePlanStep, x), x)], self.phase_plan) if self.plan_version is not None: result["planVersion"] = from_union([from_none, from_str], self.plan_version) if self.policy_version is not None: @@ -4109,9 +4254,65 @@ def to_dict(self) -> dict: return result +@dataclass +class CompletionReceiptEventRange: + "Inclusive durable event range summarized by a completion receipt." + end_event_id: str + start_event_id: str + + @staticmethod + def from_dict(obj: Any) -> "CompletionReceiptEventRange": + assert isinstance(obj, dict) + end_event_id = from_str(obj.get("endEventId")) + start_event_id = from_str(obj.get("startEventId")) + return CompletionReceiptEventRange( + end_event_id=end_event_id, + start_event_id=start_event_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["endEventId"] = from_str(self.end_event_id) + result["startEventId"] = from_str(self.start_event_id) + return result + + +@dataclass +class CompletionReceiptFinalTool: + "Final structured tool completion in the covered event range." + status: CompletionReceiptToolStatus + tool_call_id: str + exit_code: int | None = None + tool_name: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "CompletionReceiptFinalTool": + assert isinstance(obj, dict) + status = parse_enum(CompletionReceiptToolStatus, obj.get("status")) + tool_call_id = from_str(obj.get("toolCallId")) + exit_code = from_union([from_none, from_int], obj.get("exitCode")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return CompletionReceiptFinalTool( + status=status, + tool_call_id=tool_call_id, + exit_code=exit_code, + tool_name=tool_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(CompletionReceiptToolStatus, self.status) + result["toolCallId"] = from_str(self.tool_call_id) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, to_int], self.exit_code) + if self.tool_name is not None: + result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result + + @dataclass class CustomAgentsUpdatedAgent: - "A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override." + "A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration." description: str display_name: str id: str @@ -4120,6 +4321,8 @@ class CustomAgentsUpdatedAgent: tools: list[str] | None user_invocable: bool model: str | None = None + model_policy: AgentModelPolicy | None = None + models: list[str] | None = None @staticmethod def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": @@ -4132,6 +4335,8 @@ def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": tools = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("tools")) user_invocable = from_bool(obj.get("userInvocable")) model = from_union([from_none, from_str], obj.get("model")) + model_policy = from_union([from_none, lambda x: parse_enum(AgentModelPolicy, x)], obj.get("modelPolicy")) + models = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("models")) return CustomAgentsUpdatedAgent( description=description, display_name=display_name, @@ -4141,6 +4346,8 @@ def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": tools=tools, user_invocable=user_invocable, model=model, + model_policy=model_policy, + models=models, ) def to_dict(self) -> dict: @@ -4154,6 +4361,10 @@ def to_dict(self) -> dict: result["userInvocable"] = from_bool(self.user_invocable) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_policy is not None: + result["modelPolicy"] = from_union([from_none, lambda x: to_enum(AgentModelPolicy, x)], self.model_policy) + if self.models is not None: + result["models"] = from_union([from_none, lambda x: from_list(from_str, x)], self.models) return result @@ -7078,6 +7289,7 @@ class PermissionRequestedData: "Permission request notification requiring client approval with request details" permission_request: PermissionRequest request_id: str + agent_mode: SessionMode | None = None prompt_request: PermissionPromptRequest | None = None resolved_by_hook: bool | None = None risk_assessment: Any = None @@ -7087,12 +7299,14 @@ def from_dict(obj: Any) -> "PermissionRequestedData": assert isinstance(obj, dict) permission_request = _load_PermissionRequest(obj.get("permissionRequest")) request_id = from_str(obj.get("requestId")) + agent_mode = from_union([from_none, lambda x: parse_enum(SessionMode, x)], obj.get("agentMode")) prompt_request = from_union([from_none, _load_PermissionPromptRequest], obj.get("promptRequest")) resolved_by_hook = from_union([from_none, from_bool], obj.get("resolvedByHook")) risk_assessment = obj.get("riskAssessment") return PermissionRequestedData( permission_request=permission_request, request_id=request_id, + agent_mode=agent_mode, prompt_request=prompt_request, resolved_by_hook=resolved_by_hook, risk_assessment=risk_assessment, @@ -7102,6 +7316,8 @@ def to_dict(self) -> dict: result: dict = {} result["permissionRequest"] = self.permission_request.to_dict() result["requestId"] = from_str(self.request_id) + if self.agent_mode is not None: + result["agentMode"] = from_union([from_none, lambda x: to_enum(SessionMode, x)], self.agent_mode) if self.prompt_request is not None: result["promptRequest"] = from_union([from_none, lambda x: x.to_dict()], self.prompt_request) if self.resolved_by_hook is not None: @@ -9312,6 +9528,7 @@ class SubagentCompletedData: explicit_model_override: str | None = None first_dispatched_model: str | None = None model: str | None = None + model_override_reason: str | None = None total_tokens: int | None = None total_tool_calls: int | None = None @@ -9329,6 +9546,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData": explicit_model_override = from_union([from_none, from_str], obj.get("explicitModelOverride")) first_dispatched_model = from_union([from_none, from_str], obj.get("firstDispatchedModel")) model = from_union([from_none, from_str], obj.get("model")) + model_override_reason = from_union([from_none, from_str], obj.get("modelOverrideReason")) total_tokens = from_union([from_none, from_int], obj.get("totalTokens")) total_tool_calls = from_union([from_none, from_int], obj.get("totalToolCalls")) return SubagentCompletedData( @@ -9343,6 +9561,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData": explicit_model_override=explicit_model_override, first_dispatched_model=first_dispatched_model, model=model, + model_override_reason=model_override_reason, total_tokens=total_tokens, total_tool_calls=total_tool_calls, ) @@ -9368,6 +9587,8 @@ def to_dict(self) -> dict: result["firstDispatchedModel"] = from_union([from_none, from_str], self.first_dispatched_model) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_override_reason is not None: + result["modelOverrideReason"] = from_union([from_none, from_str], self.model_override_reason) if self.total_tokens is not None: result["totalTokens"] = from_union([from_none, to_int], self.total_tokens) if self.total_tool_calls is not None: @@ -9434,6 +9655,7 @@ class SubagentFailedData: explicit_model_override: str | None = None first_dispatched_model: str | None = None model: str | None = None + model_override_reason: str | None = None total_tokens: int | None = None total_tool_calls: int | None = None @@ -9451,6 +9673,7 @@ def from_dict(obj: Any) -> "SubagentFailedData": explicit_model_override = from_union([from_none, from_str], obj.get("explicitModelOverride")) first_dispatched_model = from_union([from_none, from_str], obj.get("firstDispatchedModel")) model = from_union([from_none, from_str], obj.get("model")) + model_override_reason = from_union([from_none, from_str], obj.get("modelOverrideReason")) total_tokens = from_union([from_none, from_int], obj.get("totalTokens")) total_tool_calls = from_union([from_none, from_int], obj.get("totalToolCalls")) return SubagentFailedData( @@ -9465,6 +9688,7 @@ def from_dict(obj: Any) -> "SubagentFailedData": explicit_model_override=explicit_model_override, first_dispatched_model=first_dispatched_model, model=model, + model_override_reason=model_override_reason, total_tokens=total_tokens, total_tool_calls=total_tool_calls, ) @@ -9489,6 +9713,8 @@ def to_dict(self) -> dict: result["firstDispatchedModel"] = from_union([from_none, from_str], self.first_dispatched_model) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_override_reason is not None: + result["modelOverrideReason"] = from_union([from_none, from_str], self.model_override_reason) if self.total_tokens is not None: result["totalTokens"] = from_union([from_none, to_int], self.total_tokens) if self.total_tool_calls is not None: @@ -11017,6 +11243,7 @@ class UserMessageData: delivery: UserMessageDelivery | None = None interaction_id: str | None = None is_autopilot_continuation: bool | None = None + message_id: str | None = None native_document_path_fallback_paths: list[str] | None = None parent_agent_task_id: str | None = None source: str | None = None @@ -11033,6 +11260,7 @@ def from_dict(obj: Any) -> "UserMessageData": 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")) + message_id = from_union([from_none, from_str], obj.get("messageId")) native_document_path_fallback_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("nativeDocumentPathFallbackPaths")) parent_agent_task_id = from_union([from_none, from_str], obj.get("parentAgentTaskId")) source = from_union([from_none, from_str], obj.get("source")) @@ -11046,6 +11274,7 @@ def from_dict(obj: Any) -> "UserMessageData": delivery=delivery, interaction_id=interaction_id, is_autopilot_continuation=is_autopilot_continuation, + message_id=message_id, native_document_path_fallback_paths=native_document_path_fallback_paths, parent_agent_task_id=parent_agent_task_id, source=source, @@ -11067,6 +11296,8 @@ def to_dict(self) -> dict: result["interactionId"] = from_union([from_none, from_str], self.interaction_id) if self.is_autopilot_continuation is not None: result["isAutopilotContinuation"] = from_union([from_none, from_bool], self.is_autopilot_continuation) + if self.message_id is not None: + result["messageId"] = from_union([from_none, from_str], self.message_id) if self.native_document_path_fallback_paths is not None: result["nativeDocumentPathFallbackPaths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.native_document_path_fallback_paths) if self.parent_agent_task_id is not None: @@ -11590,6 +11821,17 @@ class FusionPattern(Enum): CRITIQUE = "critique" +# Experimental: this enum is part of an experimental API and may change or be removed. +class FusionPhaseActivityKind(Enum): + "Content-safe activity observed while a HydraFusion phase is running." + # The provider produced additional private output bytes. + MODEL_OUTPUT = "model_output" + # A tool began executing inside the phase. + TOOL_STARTED = "tool_started" + # A tool finished executing inside the phase. + TOOL_COMPLETED = "tool_completed" + + # Experimental: this enum is part of an experimental API and may change or be removed. class FusionPhaseKind(Enum): "HydraFusion phase kind." @@ -11705,6 +11947,14 @@ class AgentInterruptedCancelPhase(Enum): MID_STREAM = "mid_stream" +class AgentModelPolicy(Enum): + "Whether configured models are advisory preferences or required constraints" + # Treat the authored models as advisory preferences that callers may override. + PREFERRED = "preferred" + # Require subagent execution to use one of the authored models. + REQUIRED = "required" + + class AssistantMessageToolRequestCallerType(Enum): "Hosted program caller type" PROGRAM = "program" @@ -11830,6 +12080,30 @@ class CompactionTrigger(Enum): MODEL_SWITCH = "model_switch" +class CompletionReceiptStopReason(Enum): + "Runtime reason the completion decision was accepted." + # The model reached a natural terminal response. + NATURAL = "natural" + # A terminal tool ended the interaction. + TERMINAL_TOOL = "terminal_tool" + # The configured agentStop continuation limit was reached. + AGENT_STOP_BLOCK_LIMIT = "agent_stop_block_limit" + + +class CompletionReceiptToolStatus(Enum): + "Structured terminal status from a tool completion event." + # The tool completed successfully. + SUCCESS = "success" + # The tool failed without a more specific structured status. + FAILURE = "failure" + # The tool exceeded its time budget. + TIMEOUT = "timeout" + # The user rejected the tool call. + REJECTED = "rejected" + # The permissions service denied the tool call. + DENIED = "denied" + + class ContextTier(Enum): "Allowed values for the `ContextTier` enumeration." # Default context tier with standard context window size. @@ -12366,7 +12640,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | 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 | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -12423,6 +12697,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_COMPACTION_START: data = SessionCompactionStartData.from_dict(data_obj) case SessionEventType.SESSION_COMPACTION_COMPLETE: data = SessionCompactionCompleteData.from_dict(data_obj) case SessionEventType.SESSION_TASK_COMPLETE: data = SessionTaskCompleteData.from_dict(data_obj) + case SessionEventType.SESSION_COMPLETION_RECEIPT: data = SessionCompletionReceiptData.from_dict(data_obj) case SessionEventType.SESSION_FUSION_ROUTE_STARTED: data = SessionFusionRouteStartedData.from_dict(data_obj) case SessionEventType.SESSION_FUSION_ROUTE_FAILED: data = SessionFusionRouteFailedData.from_dict(data_obj) case SessionEventType.SESSION_FUSION_RESOLVED: data = SessionFusionResolvedData.from_dict(data_obj) @@ -12434,6 +12709,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.AGENT_INTERRUPTED: data = AgentInterruptedData.from_dict(data_obj) case SessionEventType.ASSISTANT_INTENT: data = AssistantIntentData.from_dict(data_obj) case SessionEventType.ASSISTANT_FUSION_PHASE_STARTED: data = AssistantFusionPhaseStartedData.from_dict(data_obj) + case SessionEventType.ASSISTANT_FUSION_PHASE_ACTIVITY: data = AssistantFusionPhaseActivityData.from_dict(data_obj) case SessionEventType.ASSISTANT_FUSION_PHASE_COMPLETED: data = AssistantFusionPhaseCompletedData.from_dict(data_obj) case SessionEventType.ASSISTANT_FUSION_PHASE_FAILED: data = AssistantFusionPhaseFailedData.from_dict(data_obj) case SessionEventType.ASSISTANT_SERVER_TOOL_PROGRESS: data = AssistantServerToolProgressData.from_dict(data_obj) @@ -12562,6 +12838,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AgentInterruptedActivity", "AgentInterruptedCancelPhase", "AgentInterruptedData", + "AgentModelPolicy", + "AssistantFusionPhaseActivityData", "AssistantFusionPhaseCompletedData", "AssistantFusionPhaseFailedData", "AssistantFusionPhaseStartedData", @@ -12646,6 +12924,10 @@ def session_event_to_dict(x: SessionEvent) -> Any: "CompactionCompleteCompactionTokensUsed", "CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail", "CompactionTrigger", + "CompletionReceiptEventRange", + "CompletionReceiptFinalTool", + "CompletionReceiptStopReason", + "CompletionReceiptToolStatus", "ContextTier", "CustomAgentsUpdatedAgent", "Data", @@ -12675,7 +12957,9 @@ def session_event_to_dict(x: SessionEvent) -> Any: "FusionFollowUpAction", "FusionFollowUpRecommendation", "FusionPattern", + "FusionPhaseActivityKind", "FusionPhaseKind", + "FusionPhasePlanStep", "FusionPhaseStatus", "FusionPhaseUsage", "FusionScores", @@ -12801,6 +13085,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SessionCanvasUnavailableData", "SessionCompactionCompleteData", "SessionCompactionStartData", + "SessionCompletionReceiptData", "SessionContextChangedData", "SessionContextClearedData", "SessionCustomAgentsUpdatedData", diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index b1d9896f3e..3eff4e1524 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -10,11 +10,11 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use super::session_events::{ - AbortReason, AutoTier, ContextTier, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, - McpServerSource, McpServerStatus, ModelChangeSource, OmittedBinaryOmittedReason, - PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionLimitsConfig, - SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, - Verbosity, + AbortReason, AgentModelPolicy, AutoTier, ContextTier, McpOauthHttpResponse, + McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, + OmittedBinaryOmittedReason, PermissionMode, PermissionPromptRequest, PermissionRule, + ReasoningSummary, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, + TaskCompletionOutcome, UserToolSessionApproval, Verbosity, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -24,6 +24,8 @@ pub mod rpc_methods { pub const PING: &str = "ping"; /// `connect` pub const CONNECT: &str = "connect"; + /// `hooks.discover` + pub const HOOKS_DISCOVER: &str = "hooks.discover"; /// `models.list` pub const MODELS_LIST: &str = "models.list"; /// `models.getBuiltInCatalog` @@ -142,6 +144,8 @@ pub mod rpc_methods { pub const SESSIONS_LIST: &str = "sessions.list"; /// `sessions.getMetadata` pub const SESSIONS_GETMETADATA: &str = "sessions.getMetadata"; + /// `sessions.readPersistedEvents` + pub const SESSIONS_READPERSISTEDEVENTS: &str = "sessions.readPersistedEvents"; /// `sessions.listNonEmptySessionIds` pub const SESSIONS_LISTNONEMPTYSESSIONIDS: &str = "sessions.listNonEmptySessionIds"; /// `sessions.findByTaskId` @@ -1541,7 +1545,7 @@ pub struct AgentDiscoveryPathList { pub paths: Vec, } -/// Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. +/// Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. /// ///
/// @@ -1571,6 +1575,12 @@ pub struct AgentInfo { /// Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Whether authored models are preferences or required constraints. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_policy: Option, + /// Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + #[serde(skip_serializing_if = "Option::is_none")] + pub models: Option>, /// Name of the agent. Use `id` as the stable selection identifier. pub name: String, /// Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. @@ -4379,6 +4389,36 @@ pub struct DiscoveredExtensionsEnableRequest { pub ids: Vec, } +/// One server-discovered hook action from user, repository, plugin, or managed-policy configuration. +/// +///
+/// +/// **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 DiscoveredHook { + /// Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_key: Option, + /// Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + pub enabled: bool, + /// Hook event that invokes this action. + pub hook_type: HookType, + /// Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. + pub id: String, + /// Configuration tier that contributed this hook action. + pub origin: HookOrigin, + /// Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + #[serde(skip_serializing_if = "Option::is_none")] + pub project_path: Option, + /// Human-readable source label, such as a hook file path, settings source, or plugin name. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + /// MCP server discovered by `mcp.discover`, with config source, optional plugin source, transport type, and enabled state. /// ///
@@ -6459,8 +6499,7 @@ pub struct HistoryTruncateResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct HookInvokeRequest { - #[doc(hidden)] - pub(crate) hook_type: HookType, + pub hook_type: HookType, pub input: serde_json::Value, pub session_id: SessionId, } @@ -6473,6 +6512,44 @@ pub(crate) struct HookInvokeResponse { pub output: Option, } +/// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +/// +///
+/// +/// **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 HooksDiscoverRequest { + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_host_hooks: Option, + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + #[serde(skip_serializing_if = "Option::is_none")] + pub project_paths: Option>, +} + +/// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. +/// +///
+/// +/// **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 HooksDiscoverResult { + /// Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + pub errors: Vec, + /// All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + pub hooks: Vec, + /// Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + pub warnings: Vec, +} + /// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. /// ///
@@ -10519,7 +10596,7 @@ pub struct ModelSwitchToRequest { /// When true, evaluate context-window compaction policy before applying the switch. #[serde(skip_serializing_if = "Option::is_none")] pub run_compaction_preflight: Option, - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// Output verbosity level to request for supported models @@ -13974,6 +14051,9 @@ pub struct QueuePendingItems { pub id: String, /// Whether this item is a queued user message or a queued slash command / model change pub kind: QueuePendingItemsKind, + /// Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + #[serde(skip_serializing_if = "Option::is_none")] + pub message_id: Option, } /// Snapshot of the session's pending queued items and immediate-steering messages. @@ -14680,7 +14760,7 @@ pub struct SandboxConfigUserPolicyNetworkProxy { /// Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, - /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. pub url: String, /// Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. #[serde(skip_serializing_if = "Option::is_none")] @@ -14704,7 +14784,7 @@ pub struct SandboxConfigUserPolicyNetwork { /// Whether outbound network traffic is allowed at all. #[serde(skip_serializing_if = "Option::is_none")] pub allow_outbound: Option, - /// HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. + /// HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, requires that endpoint to be reachable over IPv4 (the `[::]` dual-stack wildcard is accepted and routed through the IPv4 gateway), and does not support proxy credentials. macOS relies on applications honoring proxy environment variables. Windows also configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's networking stack. Configure supported credentials in the separate `username` and `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, while an https:// or authenticated loopback URL uses the URL form. #[serde(skip_serializing_if = "Option::is_none")] pub proxy: Option, } @@ -17969,6 +18049,30 @@ pub struct SessionsPruneOldRequest { pub older_than_days: i64, } +/// Pagination options for reading an inactive or active local session's persisted event journal. +/// +///
+/// +/// **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 SessionsReadPersistedEventsRequest { + /// Opaque cursor returned by a previous persisted-event read. Omit on the first call. + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. + #[serde(skip_serializing_if = "Option::is_none")] + pub direction: Option, + /// Maximum number of events to return in this batch (1–1000, default 200). + #[serde(skip_serializing_if = "Option::is_none")] + pub max: Option, + /// Session ID whose persisted event journal should be read. + pub session_id: SessionId, +} + /// Session ID whose in-use lock should be released. /// ///
@@ -19031,6 +19135,9 @@ pub struct SubagentSettingsEntry { /// Model override for matching subagents #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Whether the configured model strategy is preferred or required + #[serde(skip_serializing_if = "Option::is_none")] + pub model_policy: Option, } /// Subagent settings to apply, or null to clear the live session override @@ -21795,6 +21902,27 @@ pub struct SessionsListResult { pub sessions: Vec, } +/// Batch of session events returned by a read, with cursor and continuation metadata. +/// +///
+/// +/// **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 SessionsReadPersistedEventsResult { + /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). + pub cursor: String, + /// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. + pub cursor_status: EventsCursorStatus, + /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. + pub events: Vec, + /// True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. + pub has_more: bool, +} + /// ID of the local session bound to the given GitHub task, or omitted when none. /// ///
@@ -28951,6 +29079,101 @@ pub enum DiscoveredExtensionMode { Unknown, } +/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum HookType { + /// Runs before a tool is invoked. + #[serde(rename = "preToolUse")] + PreToolUse, + /// Runs before an MCP tool is invoked. + #[serde(rename = "preMcpToolCall")] + PreMcpToolCall, + /// Runs after a tool completes successfully. + #[serde(rename = "postToolUse")] + PostToolUse, + /// Runs after a tool fails. + #[serde(rename = "postToolUseFailure")] + PostToolUseFailure, + /// Runs after the user submits a prompt. + #[serde(rename = "userPromptSubmitted")] + UserPromptSubmitted, + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + #[serde(rename = "userPromptTransformed")] + UserPromptTransformed, + /// Runs when a session starts. + #[serde(rename = "sessionStart")] + SessionStart, + /// Runs when a session ends. + #[serde(rename = "sessionEnd")] + SessionEnd, + /// Runs after an agent result is produced. + #[serde(rename = "postResult")] + PostResult, + /// Runs before a pull request description is generated. + #[serde(rename = "prePRDescription")] + PrePRDescription, + /// Runs when the agent encounters an error. + #[serde(rename = "errorOccurred")] + ErrorOccurred, + /// Runs when the agent stops. + #[serde(rename = "agentStop")] + AgentStop, + /// Runs when a subagent starts. + #[serde(rename = "subagentStart")] + SubagentStart, + /// Runs when a subagent stops. + #[serde(rename = "subagentStop")] + SubagentStop, + /// Runs before conversation context is compacted. + #[serde(rename = "preCompact")] + PreCompact, + /// Runs when the agent requests permission. + #[serde(rename = "permissionRequest")] + PermissionRequest, + /// Runs when the agent emits a notification. + #[serde(rename = "notification")] + Notification, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Configuration tier that contributed a discovered hook action. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum HookOrigin { + /// Hook loaded from user settings or the user's hook directory. + #[serde(rename = "user")] + User, + /// Hook loaded from repository settings or the repository hook directory. + #[serde(rename = "repository")] + Repository, + /// Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. + #[serde(rename = "plugin")] + Plugin, + /// Hook enforced by centrally managed policy. + #[serde(rename = "policy")] + Policy, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Server transport type: stdio, http, sse (deprecated), or memory /// ///
@@ -29565,66 +29788,6 @@ pub enum HistoryRewindOutcome { Unknown, } -/// Hook event name dispatched through the SDK callback transport. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum HookType { - /// Runs before a tool is invoked. - #[serde(rename = "preToolUse")] - PreToolUse, - /// Runs before an MCP tool is invoked. - #[serde(rename = "preMcpToolCall")] - PreMcpToolCall, - /// Runs after a tool completes successfully. - #[serde(rename = "postToolUse")] - PostToolUse, - /// Runs after a tool fails. - #[serde(rename = "postToolUseFailure")] - PostToolUseFailure, - /// Runs after the user submits a prompt. - #[serde(rename = "userPromptSubmitted")] - UserPromptSubmitted, - /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. - #[serde(rename = "userPromptTransformed")] - UserPromptTransformed, - /// Runs when a session starts. - #[serde(rename = "sessionStart")] - SessionStart, - /// Runs when a session ends. - #[serde(rename = "sessionEnd")] - SessionEnd, - /// Runs after an agent result is produced. - #[serde(rename = "postResult")] - PostResult, - /// Runs before a pull request description is generated. - #[serde(rename = "prePRDescription")] - PrePRDescription, - /// Runs when the agent encounters an error. - #[serde(rename = "errorOccurred")] - ErrorOccurred, - /// Runs when the agent stops. - #[serde(rename = "agentStop")] - AgentStop, - /// Runs when a subagent starts. - #[serde(rename = "subagentStart")] - SubagentStart, - /// Runs when a subagent stops. - #[serde(rename = "subagentStop")] - SubagentStop, - /// Runs before conversation context is compacted. - #[serde(rename = "preCompact")] - PreCompact, - /// Runs when the agent requests permission. - #[serde(rename = "permissionRequest")] - PermissionRequest, - /// Runs when the agent emits a notification. - #[serde(rename = "notification")] - Notification, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Constant value. Always "github". #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum InstalledPluginSourceGitHubSource { diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 6f5895d856..d31cea5ce9 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -64,6 +64,13 @@ impl<'a> ClientRpc<'a> { } } + /// `hooks.*` sub-namespace. + pub fn hooks(&self) -> ClientRpcHooks<'a> { + ClientRpcHooks { + client: self.client, + } + } + /// `instructions.*` sub-namespace. pub fn instructions(&self) -> ClientRpcInstructions<'a> { ClientRpcInstructions { @@ -656,6 +663,45 @@ impl<'a> ClientRpcExtensions<'a> { } } +/// `hooks.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcHooks<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcHooks<'a> { + /// Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + /// + /// Wire method: `hooks.discover`. + /// + /// # Parameters + /// + /// * `params` - Optional project paths and host-exclusion behavior for server-scoped hook discovery. + /// + /// # Returns + /// + /// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + /// + ///
+ /// + /// **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 discover( + &self, + params: HooksDiscoverRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::HOOKS_DISCOVER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `instructions.*` RPCs. #[derive(Clone, Copy)] pub struct ClientRpcInstructions<'a> { @@ -1886,6 +1932,37 @@ impl<'a> ClientRpcSessions<'a> { Ok(serde_json::from_value(_value)?) } + /// Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session. + /// + /// Wire method: `sessions.readPersistedEvents`. + /// + /// # Parameters + /// + /// * `params` - Pagination options for reading an inactive or active local session's persisted event journal. + /// + /// # Returns + /// + /// Batch of session events returned by a read, with cursor and continuation metadata. + /// + ///
+ /// + /// **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 read_persisted_events( + &self, + params: SessionsReadPersistedEventsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_READPERSISTEDEVENTS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. /// /// Wire method: `sessions.listNonEmptySessionIds`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 8c48ac62ca..1bf9b75903 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -87,6 +87,15 @@ pub enum SessionEventType { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "session.completion_receipt")] + SessionCompletionReceipt, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(rename = "session.fusion_route_started")] SessionFusionRouteStarted, /// @@ -144,6 +153,15 @@ pub enum SessionEventType { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "assistant.fusion_phase_activity")] + AssistantFusionPhaseActivity, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(rename = "assistant.fusion_phase_completed")] AssistantFusionPhaseCompleted, /// @@ -515,6 +533,15 @@ pub enum SessionEventData { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "session.completion_receipt")] + SessionCompletionReceipt(SessionCompletionReceiptData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(rename = "session.fusion_route_started")] SessionFusionRouteStarted(SessionFusionRouteStartedData), /// @@ -572,6 +599,15 @@ pub enum SessionEventData { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "assistant.fusion_phase_activity")] + AssistantFusionPhaseActivity(AssistantFusionPhaseActivityData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(rename = "assistant.fusion_phase_completed")] AssistantFusionPhaseCompleted(AssistantFusionPhaseCompletedData), /// @@ -1832,6 +1868,62 @@ pub struct SessionTaskCompleteData { pub summary: Option, } +/// Inclusive durable event range summarized by a completion receipt. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionReceiptEventRange { + /// Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + pub end_event_id: String, + /// Identifier of the user message that starts the covered exchange. + pub start_event_id: String, +} + +/// Final structured tool completion in the covered event range. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionReceiptFinalTool { + /// Process exit code from a structured shell result, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub exit_code: Option, + /// Structured success or failure status from the tool completion event. + pub status: CompletionReceiptToolStatus, + /// Unique identifier of the completed tool call. + pub tool_call_id: String, + /// Tool name from the matching tool execution start event, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_name: Option, +} + +/// Session event "session.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is 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 SessionCompletionReceiptData { + /// One-based accepted completion receipt ordinal in the durable session history. + pub attempt: i64, + /// Inclusive durable event range summarized by this receipt. + pub event_range: CompletionReceiptEventRange, + /// Number of failed structured tool completions in the covered range. + pub failed_tool_count: i64, + /// Final structured tool completion in the covered range, when one exists. + #[serde(skip_serializing_if = "Option::is_none")] + pub final_tool: Option, + /// Version of the completion receipt payload. + pub schema_version: i64, + /// Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + pub source_event_id: String, + /// Runtime reason the completion decision was accepted. + pub stop_reason: CompletionReceiptStopReason, + /// Number of successful structured tool completions in the covered range. + pub successful_tool_count: i64, +} + /// Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. /// ///
@@ -1901,6 +1993,27 @@ pub struct FusionFollowUpRecommendation { pub user_turn: FusionFollowUpAction, } +/// Presentation-neutral phase planned for a HydraFusion turn. +/// +///
+/// +/// **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 FusionPhasePlanStep { + /// Whether the phase executes only when an earlier phase requests it. + pub conditional: bool, + /// Kind of phase that may execute. + pub kind: FusionPhaseKind, + /// Semantic role assigned to the phase. + pub role: String, + /// Conversation scope in which the phase executes. + pub scope: FusionConversationScope, +} + /// Validated HydraFusion routing capability scores. /// ///
@@ -1949,6 +2062,16 @@ pub struct SessionFusionResolvedData { pub model_universe_version: Option, /// Validated orchestration pattern selected for the turn. pub pattern: FusionPattern, + /// Presentation-neutral phase plan for clients that render workflow progress. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub phase_plan: Option>, /// Version of the validated execution-plan format. #[serde(skip_serializing_if = "Option::is_none")] pub plan_version: Option, @@ -2056,6 +2179,9 @@ pub struct UserMessageData { /// 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. #[serde(skip_serializing_if = "Option::is_none")] pub is_autopilot_continuation: Option, + /// Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots + #[serde(skip_serializing_if = "Option::is_none")] + pub message_id: Option, /// Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit #[serde(skip_serializing_if = "Option::is_none")] pub native_document_path_fallback_paths: Option>, @@ -2186,6 +2312,39 @@ pub struct AssistantFusionPhaseStartedData { pub role: String, } +/// Session event "assistant.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase. +/// +///
+/// +/// **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 AssistantFusionPhaseActivityData { + /// Kind of real activity observed. + pub activity: FusionPhaseActivityKind, + /// Conversation scope in which the phase executes. + pub conversation_scope: FusionConversationScope, + /// Identifier of the HydraFusion turn containing the phase. + pub fusion_id: String, + /// HydraFusion orchestration pattern containing the phase. + pub pattern: FusionPattern, + /// Stable identifier for the concrete phase. + pub phase_id: String, + /// Kind of phase currently executing. + pub phase_kind: FusionPhaseKind, + /// Semantic role assigned to the phase. + pub role: String, + /// Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, + /// Cumulative private response bytes observed for this model call. The event never includes response text. + #[serde(skip_serializing_if = "Option::is_none")] + pub total_response_size_bytes: Option, +} + /// Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. /// ///
@@ -3981,6 +4140,9 @@ pub struct SubagentCompletedData { /// Model used by the sub-agent #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Why an explicit task-call model did not become the effective model + #[serde(skip_serializing_if = "Option::is_none")] + pub model_override_reason: Option, /// Tool call ID of the parent tool invocation that spawned this sub-agent pub tool_call_id: String, /// Total tokens (input + output) consumed by the sub-agent @@ -4022,6 +4184,9 @@ pub struct SubagentFailedData { /// Model selected for the sub-agent, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Why an explicit task-call model did not become the effective model + #[serde(skip_serializing_if = "Option::is_none")] + pub model_override_reason: Option, /// Tool call ID of the parent tool invocation that spawned this sub-agent pub tool_call_id: String, /// Total tokens (input + output) consumed before the sub-agent failed @@ -5030,6 +5195,9 @@ pub struct PermissionPromptRequestExtensionEnvAccess { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PermissionRequestedData { + /// Agent mode captured from the owning turn when permission evaluation began. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_mode: Option, /// Details of the permission being requested pub permission_request: PermissionRequest, /// Derived user-facing permission prompt details for UI consumers @@ -5980,7 +6148,7 @@ pub struct SessionSkillsLoadedData { pub skills: Vec, } -/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. +/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CustomAgentsUpdatedAgent { @@ -5993,6 +6161,12 @@ pub struct CustomAgentsUpdatedAgent { /// Model override for this agent, if set #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Whether authored models are preferences or required constraints + #[serde(skip_serializing_if = "Option::is_none")] + pub model_policy: Option, + /// Authored model ids in priority order, if configured + #[serde(skip_serializing_if = "Option::is_none")] + pub models: Option>, /// Internal name of the agent pub name: String, /// Source location: user, project, inherited, remote, or plugin @@ -6686,6 +6860,48 @@ pub enum TaskCompletionOutcome { Unknown, } +/// Structured terminal status from a tool completion event. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CompletionReceiptToolStatus { + /// The tool completed successfully. + #[serde(rename = "success")] + Success, + /// The tool failed without a more specific structured status. + #[serde(rename = "failure")] + Failure, + /// The tool exceeded its time budget. + #[serde(rename = "timeout")] + Timeout, + /// The user rejected the tool call. + #[serde(rename = "rejected")] + Rejected, + /// The permissions service denied the tool call. + #[serde(rename = "denied")] + Denied, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Runtime reason the completion decision was accepted. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CompletionReceiptStopReason { + /// The model reached a natural terminal response. + #[serde(rename = "natural")] + Natural, + /// A terminal tool ended the interaction. + #[serde(rename = "terminal_tool")] + TerminalTool, + /// The configured agentStop continuation limit was reached. + #[serde(rename = "agent_stop_block_limit")] + AgentStopBlockLimit, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Kind of turn for which HydraFusion routing is running. /// ///
@@ -6755,6 +6971,65 @@ pub enum FusionPattern { Unknown, } +/// HydraFusion phase kind. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionPhaseKind { + /// Primary solver phase. + #[serde(rename = "primary")] + Primary, + /// Read-only cascade judge phase. + #[serde(rename = "judge")] + Judge, + /// Cascade repair phase. + #[serde(rename = "repair")] + Repair, + /// Initial critique-pattern draft phase. + #[serde(rename = "draft")] + Draft, + /// Read-only critique phase. + #[serde(rename = "critic")] + Critic, + /// Critique-pattern revision phase. + #[serde(rename = "revision")] + Revision, + /// Follow-up phase continuing from the resolved model. + #[serde(rename = "follow_up")] + FollowUp, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Conversation scope in which a HydraFusion phase executes. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionConversationScope { + /// Canonical root conversation history. + #[serde(rename = "root")] + Root, + /// Isolated read-only review history that does not enter the root conversation. + #[serde(rename = "review")] + Review, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The agent mode that was active when this message was sent #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum UserMessageAgentMode { @@ -6845,7 +7120,7 @@ pub enum ModelCallFailureTransport { Unknown, } -/// Conversation scope in which a HydraFusion phase executes. +/// Content-safe activity observed while a HydraFusion phase is running. /// ///
/// @@ -6854,50 +7129,16 @@ pub enum ModelCallFailureTransport { /// ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionConversationScope { - /// Canonical root conversation history. - #[serde(rename = "root")] - Root, - /// Isolated read-only review history that does not enter the root conversation. - #[serde(rename = "review")] - Review, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// HydraFusion phase kind. -/// -///
-/// -/// **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, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionPhaseKind { - /// Primary solver phase. - #[serde(rename = "primary")] - Primary, - /// Read-only cascade judge phase. - #[serde(rename = "judge")] - Judge, - /// Cascade repair phase. - #[serde(rename = "repair")] - Repair, - /// Initial critique-pattern draft phase. - #[serde(rename = "draft")] - Draft, - /// Read-only critique phase. - #[serde(rename = "critic")] - Critic, - /// Critique-pattern revision phase. - #[serde(rename = "revision")] - Revision, - /// Follow-up phase continuing from the resolved model. - #[serde(rename = "follow_up")] - FollowUp, +pub enum FusionPhaseActivityKind { + /// The provider produced additional private output bytes. + #[serde(rename = "model_output")] + ModelOutput, + /// A tool began executing inside the phase. + #[serde(rename = "tool_started")] + ToolStarted, + /// A tool finished executing inside the phase. + #[serde(rename = "tool_completed")] + ToolCompleted, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -8302,6 +8543,21 @@ pub enum SkillSource { Unknown, } +/// Whether configured models are advisory preferences or required constraints +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AgentModelPolicy { + /// Treat the authored models as advisory preferences that callers may override. + #[serde(rename = "preferred")] + Preferred, + /// Require subagent execution to use one of the authored models. + #[serde(rename = "required")] + Required, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Configuration source: user, workspace, plugin, or builtin #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum McpServerSource { diff --git a/rust/tests/e2e/rpc_session_state_extras.rs b/rust/tests/e2e/rpc_session_state_extras.rs index 85c20debc7..e764c53933 100644 --- a/rust/tests/e2e/rpc_session_state_extras.rs +++ b/rust/tests/e2e/rpc_session_state_extras.rs @@ -495,6 +495,7 @@ async fn should_update_and_clear_live_subagent_settings() { ), effort_level: Some("low".to_string()), model: Some("gpt-5-mini".to_string()), + model_policy: None, }, )])), disabled_subagents: Some(vec!["legacy-agent".to_string()]), diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 0feec5e98a..b3cc5d5753 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -378,9 +378,7 @@ function tryEmitRustUnion( const lines: string[] = []; if (schema.description) { - for (const line of schema.description.split(/\r?\n/)) { - lines.push(`/// ${line}`); - } + pushRustDoc(lines, schema.description); } pushRustExperimentalDocs(lines, isSchemaExperimental(schema) || ctx.experimentalTypeNames.has(enumName)); lines.push("#[derive(Debug, Clone, Serialize, Deserialize)]"); @@ -468,7 +466,8 @@ function pushRustExperimentalDocs( function pushRustDoc(lines: string[], text: string | undefined, indent = ""): void { if (!text) return; - for (const paragraph of text.trim().split(/\r?\n/)) { + const sanitized = text.replace(/\[::\]/g, "`[::]`"); + for (const paragraph of sanitized.trim().split(/\r?\n/)) { if (paragraph.trim().length === 0) { lines.push(`${indent}///`); } else { @@ -971,9 +970,7 @@ function emitRustStruct( for (const { propName, prop, isReq, rustField, rustType } of fields) { if (prop.description) { - for (const line of prop.description.split(/\r?\n/)) { - lines.push(` /// ${line}`); - } + pushRustDoc(lines, prop.description, " "); } pushRustExperimentalDocs(lines, isSchemaExperimental(prop), " "); const propIsInternal = isSchemaInternal(prop); diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 274e70f675..8cf978665f 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.83-1", + "@github/copilot": "^1.0.83-2", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -472,8 +472,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-1", - "integrity": "sha512-7rhgbgSx7IgfFYXrTx2/2+qowv6MZjC6BN0mhba37RuGQxsca9Zk2ucAeIX3GbQTp91AhvnN0aHCxabX36xADQ==", + "version": "1.0.83-2", + "integrity": "sha512-ntRvwGdZbZJjOvdV3LVBQ7z69W19DO4MWe/dh6pP8kYeumbzC4udVj7mCtoSwBOs0hwXirhXY+BZwqSJUp5FLQ==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -483,19 +483,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-1", - "@github/copilot-darwin-x64": "1.0.83-1", - "@github/copilot-linux-arm64": "1.0.83-1", - "@github/copilot-linux-x64": "1.0.83-1", - "@github/copilot-linuxmusl-arm64": "1.0.83-1", - "@github/copilot-linuxmusl-x64": "1.0.83-1", - "@github/copilot-win32-arm64": "1.0.83-1", - "@github/copilot-win32-x64": "1.0.83-1" + "@github/copilot-darwin-arm64": "1.0.83-2", + "@github/copilot-darwin-x64": "1.0.83-2", + "@github/copilot-linux-arm64": "1.0.83-2", + "@github/copilot-linux-x64": "1.0.83-2", + "@github/copilot-linuxmusl-arm64": "1.0.83-2", + "@github/copilot-linuxmusl-x64": "1.0.83-2", + "@github/copilot-win32-arm64": "1.0.83-2", + "@github/copilot-win32-x64": "1.0.83-2" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-1", - "integrity": "sha512-gxYJEd4yoIBIQHRmJw6ZegSkR6VuNm7AJVdAdARcU0SS68VZjtZFT29luThAp75Pxp37S5w+3A/jQNtxGyHizw==", + "version": "1.0.83-2", + "integrity": "sha512-RBjF/zTJe+gp9PsthYtIHF36+y3A3Zv3w/2FAa9nHF6ople1+lYoQ0OC1z6tWQSpBx3n/fSkNt0ebMmfhWlQyw==", "cpu": [ "arm64" ], @@ -510,8 +510,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-1", - "integrity": "sha512-PL63+Ib1RH+Bpu2UYJ9E8tmD4NZD/YlhD25F1Q9Rb86AdgLEoRYlxjNxXXF9P9dMmJ/u5zm0rUtWIRxv/FRbPg==", + "version": "1.0.83-2", + "integrity": "sha512-GzaarluCiHUA4yrYxWIFEklwPLTYjxGvLyYjI0wW5IPseT7GLFjN7AkDGRU4Ny/mxTj1MbUEsJAWVtiNy5EUHg==", "cpu": [ "x64" ], @@ -526,8 +526,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-1", - "integrity": "sha512-7c4RVEuXQu9zepAWoXwkizpESztUbWUR1hLPl+o+19L+D8fXm+5XQRH5E22Ia7P7/xHW3Ml2o2x5cXFq0k7/7w==", + "version": "1.0.83-2", + "integrity": "sha512-iGD5Pc7vnzrAjqTK/BrI8MDtMqXyxUe7P6u8TiGt+4DzKe3v2myT6eFQ+z/JJOHNpiu6LUpTDzwS8WttZqqJpA==", "cpu": [ "arm64" ], @@ -542,8 +542,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-1", - "integrity": "sha512-Be0zUVysLYV66pnOPhMxpShQ8Ox5V07qcsSOoNzG7iwpzkfZuf70U9CHk/fnI//mWwmZR+caPUfVM9BpwrdNnA==", + "version": "1.0.83-2", + "integrity": "sha512-jOQwz075vRWat+RJiMNtD+7vm4DQ83Dvw4iakpNkLfqWMPXlliVr/tqL+SffeSo87LnhOKobZOJOjELS27drdg==", "cpu": [ "x64" ], @@ -558,8 +558,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-1", - "integrity": "sha512-PZNP8vZZJU8yIc3dNjGUBUCFPNjqZs8FPZLDxvL6XdHM7Ep4cLBa+QdbRsw+nZONIaG0GtQf26eqk5ymYYpUPA==", + "version": "1.0.83-2", + "integrity": "sha512-6/+ByEBEV/vDofrCymaKrEZ5/p+TVHQfgWu8ywvWEPALfxj/iHpAF3grM2bj7uED4C19LTSarHNCkCqdrAUzIQ==", "cpu": [ "arm64" ], @@ -574,8 +574,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-1", - "integrity": "sha512-uKMhGtg5K07nA5/pxDSq8S5F/I+sMgVhUXg90Y4BI9trOidgxpXpCVyh4TeRNj2ENNugPvATdfGc6eFPsjmlgQ==", + "version": "1.0.83-2", + "integrity": "sha512-8dbiPUHaDemDibLfKXDe5xublJxt6fOht4yYDk/ikmOGAVezBUwRVO27PchXQBwGjP2PVNAASkv2WB4Ubw72lA==", "cpu": [ "x64" ], @@ -590,8 +590,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-1", - "integrity": "sha512-vMxHTmv3SotBuNI4LKcIFckd+n0OvhTik27qjsqOnVqFpvmHDghSsa8YPxySyxUQIE35z5pJaivVBK3dfEAY7w==", + "version": "1.0.83-2", + "integrity": "sha512-ZE1iUXlJSnNIH+VoRG96emy2e7jMJm85/pcKCrgLvqhDAcQSqiqLVv3OJp2VX2IcDxToOpiaGccU8Ab9zHQqkQ==", "cpu": [ "arm64" ], @@ -606,8 +606,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-1", - "integrity": "sha512-2l0VVTazW3/+p94ZQ5sAPCj1iH7KWxJAYh4yXWfvpsB/1JEEmyVgpLei1MeaMFVdtp/hi9bGprZ9+zWNlHh5hQ==", + "version": "1.0.83-2", + "integrity": "sha512-qfturLon+1oaWqSaXCISL6BEf5K+ijJFl0m/OZwstWYSlIlaE2+iOz5LcRukoxm25OG1JZDHQFp1k2PY2A+LbA==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index e293910938..91aaff849a 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.83-1", + "@github/copilot": "^1.0.83-2", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", From 1fa56bf14a81e95731f75a5af3a1ac3b9c61f47b Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Wed, 2 Sep 2026 17:57:37 -0700 Subject: [PATCH 32/60] Fix internal client-session handler codegen (#2494) * Fix internal client-session handler codegen Filter internal client-session methods from the generated TypeScript, Go, and Python public handler surfaces. Add cross-language regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 833a7dff-c7df-4928-aad3-4850afb38cf4 * Preserve empty client handler scaffolding Keep stable generated client-session handler containers when visibility filtering removes every method, and cover the all-internal case across TypeScript, Go, and Python. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 833a7dff-c7df-4928-aad3-4850afb38cf4 --------- Copilot-Session: 833a7dff-c7df-4928-aad3-4850afb38cf4 --- nodejs/test/client-api-codegen.test.ts | 115 +++++++++++++++++++++++++ scripts/codegen/go.ts | 34 +++++--- scripts/codegen/python.ts | 5 +- scripts/codegen/typescript.ts | 10 ++- 4 files changed, 144 insertions(+), 20 deletions(-) create mode 100644 nodejs/test/client-api-codegen.test.ts diff --git a/nodejs/test/client-api-codegen.test.ts b/nodejs/test/client-api-codegen.test.ts new file mode 100644 index 0000000000..9331ad7689 --- /dev/null +++ b/nodejs/test/client-api-codegen.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from "vitest"; + +import { emitClientSessionApiRegistration as emitGoClientSessionApiRegistration } from "../../scripts/codegen/go.ts"; +import { emitClientSessionApiRegistration as emitPythonClientSessionApiRegistration } from "../../scripts/codegen/python.ts"; +import { emitClientSessionApiRegistration as emitTypeScriptClientSessionApiRegistration } from "../../scripts/codegen/typescript.ts"; + +const clientSessionSchema: Record = { + mixed: { + visible: { + rpcMethod: "mixed.visible", + params: { + type: "object", + title: "VisibleRequest", + properties: { + sessionId: { type: "string" }, + }, + required: ["sessionId"], + }, + result: { + type: "object", + title: "VisibleResult", + properties: {}, + }, + }, + secret: { + rpcMethod: "mixed.secret", + visibility: "internal", + params: { + $ref: "#/definitions/InternalRequest", + }, + result: { + $ref: "#/definitions/InternalResult", + }, + }, + }, + internalOnly: { + hidden: { + rpcMethod: "internalOnly.hidden", + visibility: "internal", + params: { + $ref: "#/definitions/InternalRequest", + }, + result: { + $ref: "#/definitions/InternalResult", + }, + }, + }, +}; + +const allInternalClientSessionSchema: Record = { + internalOnly: clientSessionSchema.internalOnly, +}; + +function expectOnlyPublicClientSessionHandlers(code: string): void { + expect(code).toContain("mixed.visible"); + expect(code).not.toContain("mixed.secret"); + expect(code).not.toContain("internalOnly.hidden"); + expect(code).not.toContain("InternalRequest"); + expect(code).not.toContain("InternalResult"); +} + +describe("client-session API codegen", () => { + it("excludes internal methods from TypeScript handlers", () => { + const code = emitTypeScriptClientSessionApiRegistration(clientSessionSchema).join("\n"); + const allInternalCode = emitTypeScriptClientSessionApiRegistration( + allInternalClientSessionSchema + ).join("\n"); + + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).toContain("export interface ClientSessionApiHandlers {"); + expect(allInternalCode).toContain("export function registerClientSessionApiHandlers("); + expect(allInternalCode).not.toContain("InternalOnlyHandler"); + }); + + it("excludes internal methods from Go handlers", () => { + const lines: string[] = []; + emitGoClientSessionApiRegistration(lines, clientSessionSchema, (name) => name, new Map()); + const code = lines.join("\n"); + const allInternalLines: string[] = []; + emitGoClientSessionApiRegistration( + allInternalLines, + allInternalClientSessionSchema, + (name) => name, + new Map() + ); + const allInternalCode = allInternalLines.join("\n"); + + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).toContain("type ClientSessionAPIHandlers struct {"); + expect(allInternalCode).toContain("func RegisterClientSessionAPIHandlers("); + expect(allInternalCode).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).not.toContain("clientSessionHandlerError"); + }); + + it("excludes internal methods from Python handlers", () => { + const lines: string[] = []; + emitPythonClientSessionApiRegistration(lines, clientSessionSchema, (name) => name); + const code = lines.join("\n"); + const allInternalLines: string[] = []; + emitPythonClientSessionApiRegistration( + allInternalLines, + allInternalClientSessionSchema, + (name) => name + ); + const allInternalCode = allInternalLines.join("\n"); + + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).toContain("class ClientSessionApiHandlers:"); + expect(allInternalCode).toContain("def register_client_session_api_handlers("); + expect(allInternalCode).not.toContain("InternalOnlyHandler"); + }); +}); diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index acdea09727..b4297a239c 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -3967,7 +3967,10 @@ async function generateRpc(schemaPath?: string): Promise { if (generatedTypeCode.includes("time.Time")) { imports.push(`"time"`); } - if (schema.clientSession || schema.clientGlobal) { + const publicClientSession = schema.clientSession + ? filterNodeByVisibility(schema.clientSession, "public") + : null; + if (publicClientSession || schema.clientGlobal) { imports.push(`"errors"`, `"fmt"`); } imports.push(`"github.com/github/copilot-sdk/go/internal/jsonrpc2"`); @@ -4268,8 +4271,9 @@ function clientHandlerMethodName(rpcMethod: string): string { return toPascalCase(rpcMethod.split(".").at(-1)!); } -function emitClientSessionApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string, unionInfos: Map): void { - const groups = collectClientGroups(clientSchema); +export function emitClientSessionApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string, unionInfos: Map): void { + const publicClientSchema = filterNodeByVisibility(clientSchema, "public") ?? {}; + const groups = collectClientGroups(publicClientSchema); for (const { groupName, groupNode, methods } of groups) { const interfaceName = clientHandlerInterfaceName(groupName); @@ -4324,17 +4328,19 @@ function emitClientSessionApiRegistration(lines: string[], clientSchema: Record< lines.push(`}`); lines.push(``); - lines.push(`func clientSessionHandlerError(err error) *jsonrpc2.Error {`); - lines.push(`\tif err == nil {`); - lines.push(`\t\treturn nil`); - lines.push(`\t}`); - lines.push(`\tvar rpcErr *jsonrpc2.Error`); - lines.push(`\tif errors.As(err, &rpcErr) {`); - lines.push(`\t\treturn rpcErr`); - lines.push(`\t}`); - lines.push(`\treturn &jsonrpc2.Error{Code: -32603, Message: err.Error()}`); - lines.push(`}`); - lines.push(``); + if (groups.length > 0) { + lines.push(`func clientSessionHandlerError(err error) *jsonrpc2.Error {`); + lines.push(`\tif err == nil {`); + lines.push(`\t\treturn nil`); + lines.push(`\t}`); + lines.push(`\tvar rpcErr *jsonrpc2.Error`); + lines.push(`\tif errors.As(err, &rpcErr) {`); + lines.push(`\t\treturn rpcErr`); + lines.push(`\t}`); + lines.push(`\treturn &jsonrpc2.Error{Code: -32603, Message: err.Error()}`); + lines.push(`}`); + lines.push(``); + } lines.push(`// RegisterClientSessionAPIHandlers registers handlers for server-to-client session API calls.`); lines.push(`func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func(sessionID string) *ClientSessionAPIHandlers) {`); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index d4be520fe7..b3bfcf8bc9 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -3773,12 +3773,13 @@ function clientSessionHandlerMethodName(rpcMethod: string): string { return toSnakeCase(parts[parts.length - 1]); } -function emitClientSessionApiRegistration( +export function emitClientSessionApiRegistration( lines: string[], node: Record, resolveType: (name: string) => string ): void { - const groups = Object.entries(node).filter(([, value]) => typeof value === "object" && value !== null && !isRpcMethod(value)); + const publicNode = filterNodeByVisibility(node, "public") ?? {}; + const groups = Object.entries(publicNode).filter(([, value]) => typeof value === "object" && value !== null && !isRpcMethod(value)); for (const [groupName, groupNode] of groups) { const handlerName = `${toPascalCase(groupName)}Handler`; diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 4984816d8d..f5e8acb146 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -25,6 +25,7 @@ import { collectExperimentalOnlyRpcReferencedDefinitionNames, collectReachableDefinitionNames, collectRpcMethodReferencedDefinitionNames, + filterNodeByVisibility, findSharedSchemaDefinitions, hasSchemaPayload, parseExternalSchemaRef, @@ -1076,15 +1077,16 @@ function handlerMethodName(rpcMethod: string): string { * `getHandler` callback that resolves a sessionId to a handler object. * Param types include sessionId — handler code can simply ignore it. */ -function emitClientSessionApiRegistration(clientSchema: Record): string[] { +export function emitClientSessionApiRegistration(clientSchema: Record): string[] { const lines: string[] = []; - const groups = collectClientGroups(clientSchema); + const publicClientSchema = filterNodeByVisibility(clientSchema, "public") ?? {}; + const groups = collectClientGroups(publicClientSchema); // Emit a handler interface per group for (const [groupName, methods] of groups) { const interfaceName = toPascalCase(groupName) + "Handler"; - const groupDeprecated = isNodeFullyDeprecated(clientSchema[groupName] as Record); - const groupExperimental = isNodeFullyExperimental(clientSchema[groupName] as Record); + const groupDeprecated = isNodeFullyDeprecated(publicClientSchema[groupName] as Record); + const groupExperimental = isNodeFullyExperimental(publicClientSchema[groupName] as Record); if (groupDeprecated) { lines.push(`/** @deprecated Handler for \`${groupName}\` client session API methods. */`); } else if (groupExperimental) { From c672e716473da0be4da7ad807df8d21259623640 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:50:19 +0000 Subject: [PATCH 33/60] Update @github/copilot to 1.0.83-3 (#2495) - Updated nodejs and test harness dependencies - Re-ran code generators - Formatted generated code Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 10 +- dotnet/src/Generated/SessionEvents.cs | 16 +- go/rpc/zrpc.go | 87 +++++++- go/rpc/zrpc_encoding.go | 4 + go/rpc/zsession_events.go | 8 +- go/zsession_events.go | 1 + java/pom.xml | 2 +- java/scripts/codegen/package-lock.json | 72 +++---- java/scripts/codegen/package.json | 2 +- .../copilot/generated/SkillInvokedEvent.java | 6 +- .../github/copilot/generated/SkillSource.java | 6 +- .../copilot/generated/SkillsLoadedSkill.java | 2 +- .../generated/rpc/SessionOpenOptions.java | 4 + .../rpc/SessionOptionsUpdateParams.java | 2 +- .../rpc/SkillProviderDescriptor.java | 35 ++++ .../rpc/SkillProviderListParams.java | 30 +++ .../rpc/SkillProviderListResult.java | 31 +++ .../rpc/SkillProviderReadParams.java | 32 +++ .../rpc/SkillProviderReadResult.java | 30 +++ .../copilot/generated/rpc/SkillSource.java | 6 +- .../generated/rpc/SkillsInvokedSkill.java | 4 +- nodejs/package-lock.json | 54 ++--- nodejs/package.json | 2 +- nodejs/samples/package-lock.json | 2 +- nodejs/src/generated/rpc.ts | 109 +++++++++- nodejs/src/generated/session-events.ts | 14 +- python/copilot/generated/rpc.py | 188 +++++++++++++++++- python/copilot/generated/session_events.py | 9 +- rust/src/generated/api_types.rs | 113 ++++++++++- rust/src/generated/session_events.rs | 14 +- test/harness/package-lock.json | 54 ++--- test/harness/package.json | 2 +- 32 files changed, 813 insertions(+), 138 deletions(-) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 06a717aa86..c4428b46f8 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -9267,6 +9267,10 @@ public sealed class SkillsInvokedSkill [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; + /// Whether model invocation was disabled when this skill was invoked. + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } + /// Turn number when the skill was invoked. [JsonPropertyName("invokedAtTurn")] public long InvokedAtTurn { get; set; } @@ -9275,7 +9279,7 @@ public sealed class SkillsInvokedSkill [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Path to the SKILL.md file. + /// Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity. [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; } @@ -11549,7 +11553,7 @@ internal sealed class SessionUpdateOptionsParams [JsonPropertyName("enableSessionStore")] public bool? EnableSessionStore { get; set; } - /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. [JsonPropertyName("enableSkills")] public bool? EnableSkills { get; set; } @@ -33808,7 +33812,7 @@ internal OptionsApi(CopilotSession session) /// Whether to enable loading of `.github/hooks/` filesystem hooks. Separate from the SDK callback hook mechanism. /// Whether to enable host git operations (context resolution, child repo scanning, git info in system prompt). /// Whether to enable cross-session store writes and reads. - /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. /// 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 session limits. Pass null to clear the session limits. /// The to monitor for cancellation requests. The default is . diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index f832b95d85..147e08e458 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -4616,6 +4616,11 @@ public sealed partial class SkillInvokedData [JsonPropertyName("description")] public string? Description { get; set; } + /// Whether model invocation is disabled for this skill. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } + /// Model identifier active when the skill was invoked, when known. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("model")] @@ -4625,7 +4630,7 @@ public sealed partial class SkillInvokedData [JsonPropertyName("name")] public required string Name { get; set; } - /// File path to the SKILL.md definition. + /// File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity. [JsonPropertyName("path")] public required string Path { get; set; } @@ -4639,7 +4644,7 @@ public sealed partial class SkillInvokedData [JsonPropertyName("pluginVersion")] public string? PluginVersion { get; set; } - /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill). + /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] public string? Source { get; set; } @@ -10356,7 +10361,7 @@ public sealed partial class SkillsLoadedSkill [JsonPropertyName("path")] public string? Path { get; set; } - /// Source location type (e.g., project, personal-copilot, plugin, builtin). + /// Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk). [JsonPropertyName("source")] public required SkillSource Source { get; set; } @@ -15579,7 +15584,7 @@ public override void Write(Utf8JsonWriter writer, FactoryRunSettledStatus value, } } -/// Source location type (e.g., project, personal-copilot, plugin, builtin). +/// Source location type (e.g., project, personal-copilot, plugin, builtin, sdk). [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct SkillSource : IEquatable @@ -15619,6 +15624,9 @@ public SkillSource(string value) /// Skill bundled with the runtime. public static SkillSource Builtin { get; } = new("builtin"); + /// Pathless skill supplied lazily by an SDK skill provider. + public static SkillSource Sdk { get; } = new("sdk"); + /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(SkillSource left, SkillSource right) => left.Equals(right); diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 70f3bcbbb4..5b4cdab32d 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -12057,6 +12057,9 @@ type SessionOpenOptions struct { EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` // Whether shell-script safety heuristics are enabled. EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` + // Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by + // default. + EnableSkills *bool `json:"enableSkills,omitempty"` // Whether model responses stream as delta events. EnableStreaming *bool `json:"enableStreaming,omitempty"` // How MCP server environment values are interpreted. @@ -12080,6 +12083,16 @@ type SessionOpenOptions struct { ExpAssignments any `json:"expAssignments,omitempty"` // Feature-flag values resolved by the host. FeatureFlags map[string]bool `json:"featureFlags,omitzero"` + // Whether the requesting SDK session has a skill provider. The provider remains ephemeral + // and is never persisted in session options or history. When enableSkills is false, it + // remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw + // sessions.open flows reject it because they cannot safely pre-register the callback + // handler. + // Experimental: HasSkillProvider is part of an experimental API and may change or be + // removed. + // Internal: HasSkillProvider is part of the SDK's internal API surface and is not intended + // for external use. + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` // Built-in subagent names to include in this session. When specified, only these built-ins // are available, subject to runtime availability and exclusions. Custom agents with the // same name remain available. @@ -13179,8 +13192,9 @@ type SessionUpdateOptionsParams struct { EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` // Whether to enable cross-session store writes and reads. EnableSessionStore *bool `json:"enableSessionStore,omitempty"` - // Whether to enable skill directory scanning and loading. Falls back to - // enableConfigDiscovery when unset. + // Whether skill loading is enabled. Explicit false disables every source, including a bound + // SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, + // creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. EnableSkills *bool `json:"enableSkills,omitempty"` // Whether to stream model responses. EnableStreaming *bool `json:"enableStreaming,omitempty"` @@ -13598,6 +13612,66 @@ type SkillList struct { Skills []Skill `json:"skills"` } +// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched +// separately and lazily. +// Experimental: SkillProviderDescriptor is part of an experimental API and may change or be +// removed. +type SkillProviderDescriptor struct { + // Optional freeform argument hint used by slash-command catalogs. + ArgumentHint *string `json:"argumentHint,omitempty"` + // Description used in skill catalogs without fetching content. + Description string `json:"description"` + // Whether model invocation is disabled. Defaults to false. + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` + // Invocation and display name. + Name string `json:"name"` + // Whether users may invoke the skill directly. Defaults to true. + UserInvocable *bool `json:"userInvocable,omitempty"` +} + +// Identifies the target session. +// Experimental: SkillProviderListRequest is part of an experimental API and may change or +// be removed. +type SkillProviderListRequest struct { + // Target session identifier + SessionID string `json:"sessionId"` +} + +// Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to +// 1024 descriptors and 1 MiB of aggregate metadata. +// Experimental: SkillProviderListResult is part of an experimental API and may change or be +// removed. +// Internal: SkillProviderListResult is an internal SDK API and is not part of the public +// surface. +type SkillProviderListResult struct { + // Skill descriptors in provider order. Invocation names must be unique under + // case-insensitive comparison. + Skills []SkillProviderDescriptor `json:"skills"` +} + +// Identifies one SDK-provided skill by invocation name. +// Experimental: SkillProviderReadRequest is part of an experimental API and may change or +// be removed. +// Internal: SkillProviderReadRequest is an internal SDK API and is not part of the public +// surface. +type SkillProviderReadRequest struct { + // Invocation name of the skill to read. + Name string `json:"name"` + // Target session identifier + SessionID string `json:"sessionId"` +} + +// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related +// files and assets are not supported. +// Experimental: SkillProviderReadResult is part of an experimental API and may change or be +// removed. +// Internal: SkillProviderReadResult is an internal SDK API and is not part of the public +// surface. +type SkillProviderReadResult struct { + // Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. + Markdown string `json:"markdown"` +} + // Skill names to mark as disabled in global configuration, replacing any previous list. // Experimental: SkillsConfigSetDisabledSkillsRequest is part of an experimental API and may // change or be removed. @@ -13684,11 +13758,14 @@ type SkillsInvokedSkill struct { AllowedTools []string `json:"allowedTools,omitzero"` // Full content of the skill file Content string `json:"content"` + // Whether model invocation was disabled when this skill was invoked + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` // Turn number when the skill was invoked InvokedAtTurn int64 `json:"invokedAtTurn"` // Unique identifier for the skill Name string `json:"name"` - // Path to the SKILL.md file + // Path to the SKILL.md file, or an empty string for an SDK-provided skill without a + // filesystem identity Path string `json:"path"` } @@ -18809,7 +18886,7 @@ const ( SkillDiscoveryScopeProject SkillDiscoveryScope = "project" ) -// Source location type (e.g., project, personal-copilot, plugin, builtin) +// Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) // Experimental: SkillSource is part of an experimental API and may change or be removed. type SkillSource string @@ -18828,6 +18905,8 @@ const ( SkillSourcePlugin SkillSource = "plugin" // Skill defined in the current project's skill directories. SkillSourceProject SkillSource = "project" + // Pathless skill supplied lazily by an SDK skill provider. + SkillSourceSDK SkillSource = "sdk" ) // Optional completion hint for the input (e.g. 'directory' for filesystem path completion) diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 197e5b5437..9242a889a8 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -5401,6 +5401,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` + EnableSkills *bool `json:"enableSkills,omitempty"` EnableStreaming *bool `json:"enableStreaming,omitempty"` EnvValueMode *SessionOpenOptionsEnvValueMode `json:"envValueMode,omitempty"` EventsLogDirectory *string `json:"eventsLogDirectory,omitempty"` @@ -5409,6 +5410,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { ExcludedTools []string `json:"excludedTools,omitzero"` ExpAssignments any `json:"expAssignments,omitempty"` FeatureFlags map[string]bool `json:"featureFlags,omitzero"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` IncludedBuiltinSkills []string `json:"includedBuiltinSkills,omitzero"` InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` @@ -5481,6 +5483,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.EnableManagedSettings = raw.EnableManagedSettings r.EnableOnDemandInstructionDiscovery = raw.EnableOnDemandInstructionDiscovery r.EnableScriptSafety = raw.EnableScriptSafety + r.EnableSkills = raw.EnableSkills r.EnableStreaming = raw.EnableStreaming r.EnvValueMode = raw.EnvValueMode r.EventsLogDirectory = raw.EventsLogDirectory @@ -5489,6 +5492,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.ExcludedTools = raw.ExcludedTools r.ExpAssignments = raw.ExpAssignments r.FeatureFlags = raw.FeatureFlags + r.HasSkillProvider = raw.HasSkillProvider r.IncludedBuiltinAgents = raw.IncludedBuiltinAgents r.IncludedBuiltinSkills = raw.IncludedBuiltinSkills r.InstalledPlugins = raw.InstalledPlugins diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index a7d9d43af3..63709921d8 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -2402,17 +2402,19 @@ type SkillInvokedData struct { Content string `json:"content"` // Description of the skill from its SKILL.md frontmatter Description *string `json:"description,omitempty"` + // Whether model invocation is disabled for this skill + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` // Model identifier active when the skill was invoked, when known Model *string `json:"model,omitempty"` // Name of the invoked skill Name string `json:"name"` - // File path to the SKILL.md definition + // File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity Path string `json:"path"` // Name of the plugin this skill originated from, when applicable PluginName *string `json:"pluginName,omitempty"` // Version of the plugin this skill originated from, when applicable PluginVersion *string `json:"pluginVersion,omitempty"` - // Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + // Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) Source *string `json:"source,omitempty"` // What triggered the skill invocation: `user-invoked` (explicit user action, such as via a slash command or UI affordance), `agent-invoked` (agent requested the skill), or `context-load` (loaded as part of another context, such as preloading skills configured on a custom agent or subagent) Trigger *SkillInvokedTrigger `json:"trigger,omitempty"` @@ -4463,7 +4465,7 @@ type SkillsLoadedSkill struct { Name string `json:"name"` // Absolute path to the skill file, if available Path *string `json:"path,omitempty"` - // Source location type (e.g., project, personal-copilot, plugin, builtin) + // Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk) Source SkillSource `json:"source"` // Whether the skill can be invoked by the user as a slash command UserInvocable bool `json:"userInvocable"` diff --git a/go/zsession_events.go b/go/zsession_events.go index 8e8371cade..b5f6fa34f0 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -843,6 +843,7 @@ const ( SkillSourcePersonalCopilot = rpc.SkillSourcePersonalCopilot SkillSourcePlugin = rpc.SkillSourcePlugin SkillSourceProject = rpc.SkillSourceProject + SkillSourceSDK = rpc.SkillSourceSDK SystemMessageRoleDeveloper = rpc.SystemMessageRoleDeveloper SystemMessageRoleSystem = rpc.SystemMessageRoleSystem SystemNotificationAgentCompletedStatusCompleted = rpc.SystemNotificationAgentCompletedStatusCompleted diff --git a/java/pom.xml b/java/pom.xml index 7bbc1c8ce6..a9e6836242 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -63,7 +63,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.83-2 + ^1.0.83-3 true diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 31e908aca7..44a6d9fe9d 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.83-2", + "@github/copilot": "^1.0.83-3", "json-schema": "^0.4.0", "tsx": "^4.23.13" } @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-2", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-2.tgz", - "integrity": "sha512-ntRvwGdZbZJjOvdV3LVBQ7z69W19DO4MWe/dh6pP8kYeumbzC4udVj7mCtoSwBOs0hwXirhXY+BZwqSJUp5FLQ==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-3.tgz", + "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-2", - "@github/copilot-darwin-x64": "1.0.83-2", - "@github/copilot-linux-arm64": "1.0.83-2", - "@github/copilot-linux-x64": "1.0.83-2", - "@github/copilot-linuxmusl-arm64": "1.0.83-2", - "@github/copilot-linuxmusl-x64": "1.0.83-2", - "@github/copilot-win32-arm64": "1.0.83-2", - "@github/copilot-win32-x64": "1.0.83-2" + "@github/copilot-darwin-arm64": "1.0.83-3", + "@github/copilot-darwin-x64": "1.0.83-3", + "@github/copilot-linux-arm64": "1.0.83-3", + "@github/copilot-linux-x64": "1.0.83-3", + "@github/copilot-linuxmusl-arm64": "1.0.83-3", + "@github/copilot-linuxmusl-x64": "1.0.83-3", + "@github/copilot-win32-arm64": "1.0.83-3", + "@github/copilot-win32-x64": "1.0.83-3" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-2.tgz", - "integrity": "sha512-RBjF/zTJe+gp9PsthYtIHF36+y3A3Zv3w/2FAa9nHF6ople1+lYoQ0OC1z6tWQSpBx3n/fSkNt0ebMmfhWlQyw==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-3.tgz", + "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-2.tgz", - "integrity": "sha512-GzaarluCiHUA4yrYxWIFEklwPLTYjxGvLyYjI0wW5IPseT7GLFjN7AkDGRU4Ny/mxTj1MbUEsJAWVtiNy5EUHg==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-3.tgz", + "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-2.tgz", - "integrity": "sha512-iGD5Pc7vnzrAjqTK/BrI8MDtMqXyxUe7P6u8TiGt+4DzKe3v2myT6eFQ+z/JJOHNpiu6LUpTDzwS8WttZqqJpA==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-3.tgz", + "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-2.tgz", - "integrity": "sha512-jOQwz075vRWat+RJiMNtD+7vm4DQ83Dvw4iakpNkLfqWMPXlliVr/tqL+SffeSo87LnhOKobZOJOjELS27drdg==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-3.tgz", + "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-2.tgz", - "integrity": "sha512-6/+ByEBEV/vDofrCymaKrEZ5/p+TVHQfgWu8ywvWEPALfxj/iHpAF3grM2bj7uED4C19LTSarHNCkCqdrAUzIQ==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-3.tgz", + "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-2.tgz", - "integrity": "sha512-8dbiPUHaDemDibLfKXDe5xublJxt6fOht4yYDk/ikmOGAVezBUwRVO27PchXQBwGjP2PVNAASkv2WB4Ubw72lA==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-3.tgz", + "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-2.tgz", - "integrity": "sha512-ZE1iUXlJSnNIH+VoRG96emy2e7jMJm85/pcKCrgLvqhDAcQSqiqLVv3OJp2VX2IcDxToOpiaGccU8Ab9zHQqkQ==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-3.tgz", + "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-2.tgz", - "integrity": "sha512-qfturLon+1oaWqSaXCISL6BEf5K+ijJFl0m/OZwstWYSlIlaE2+iOz5LcRukoxm25OG1JZDHQFp1k2PY2A+LbA==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-3.tgz", + "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==", "cpu": [ "x64" ], diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index f5b28608a9..a9f84732a2 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.83-2", + "@github/copilot": "^1.0.83-3", "json-schema": "^0.4.0", "tsx": "^4.23.13" } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java index 6ad04f9699..e8675a6bbc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java @@ -39,13 +39,15 @@ public record SkillInvokedEventData( @JsonProperty("name") String name, /** Model identifier active when the skill was invoked, when known */ @JsonProperty("model") String model, - /** File path to the SKILL.md definition */ + /** File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity */ @JsonProperty("path") String path, /** Full content of the skill file, injected into the conversation for the model */ @JsonProperty("content") String content, /** Tool names that should be auto-approved when this skill is active */ @JsonProperty("allowedTools") List allowedTools, - /** Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) */ + /** Whether model invocation is disabled for this skill */ + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, + /** Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) */ @JsonProperty("source") String source, /** Name of the plugin this skill originated from, when applicable */ @JsonProperty("pluginName") String pluginName, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java index b681faaae8..622dc5d88a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Source location type (e.g., project, personal-copilot, plugin, builtin) + * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) * * @since 1.0.0 */ @@ -29,7 +29,9 @@ public enum SkillSource { /** The {@code custom} variant. */ CUSTOM("custom"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code sdk} variant. */ + SDK("sdk"); private final String value; SkillSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java b/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java index 932d9affe5..2335a9d9ea 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java @@ -27,7 +27,7 @@ public record SkillsLoadedSkill( @JsonProperty("commandName") String commandName, /** Description of what the skill does */ @JsonProperty("description") String description, - /** Source location type (e.g., project, personal-copilot, plugin, builtin) */ + /** Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk) */ @JsonProperty("source") SkillSource source, /** Whether the skill can be invoked by the user as a slash command */ @JsonProperty("userInvocable") Boolean userInvocable, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java index 34706a68e1..5f4753b1ac 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java @@ -111,6 +111,10 @@ public record SessionOpenOptions( @JsonProperty("allowAllMcpServerInstructions") Boolean allowAllMcpServerInstructions, /** Additional directories to search for skills. */ @JsonProperty("skillDirectories") List skillDirectories, + /** Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default. */ + @JsonProperty("enableSkills") Boolean enableSkills, + /** Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler. */ + @JsonProperty("hasSkillProvider") Boolean hasSkillProvider, /** Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. */ @JsonProperty("includedBuiltinSkills") List includedBuiltinSkills, /** Skill IDs disabled for this session. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java index 2e8a069a4b..27a5c2db06 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java @@ -140,7 +140,7 @@ public record SessionOptionsUpdateParams( @JsonProperty("enableHostGitOperations") Boolean enableHostGitOperations, /** Whether to enable cross-session store writes and reads. */ @JsonProperty("enableSessionStore") Boolean enableSessionStore, - /** Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. */ + /** Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. */ @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, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java new file mode 100644 index 0000000000..a678b95fa3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillProviderDescriptor( + /** Invocation and display name. */ + @JsonProperty("name") String name, + /** Description used in skill catalogs without fetching content. */ + @JsonProperty("description") String description, + /** Whether users may invoke the skill directly. Defaults to true. */ + @JsonProperty("userInvocable") Boolean userInvocable, + /** Whether model invocation is disabled. Defaults to false. */ + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, + /** Optional freeform argument hint used by slash-command catalogs. */ + @JsonProperty("argumentHint") String argumentHint +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java new file mode 100644 index 0000000000..e11fcf0fbd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.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; + +/** + * Identifies the target session. + * + * @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 SkillProviderListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java new file mode 100644 index 0000000000..228ec9240b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata. + * + * @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 SkillProviderListResult( + /** Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. */ + @JsonProperty("skills") List skills +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java new file mode 100644 index 0000000000..895078384b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Identifies one SDK-provided skill by invocation name. + * + * @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 SkillProviderReadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Invocation name of the skill to read. */ + @JsonProperty("name") String name +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java new file mode 100644 index 0000000000..0f17696565 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.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; + +/** + * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. + * + * @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 SkillProviderReadResult( + /** Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. */ + @JsonProperty("markdown") String markdown +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java index 8d723548be..bef1995e6c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Source location type (e.g., project, personal-copilot, plugin, builtin) + * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) * * @since 1.0.0 */ @@ -29,7 +29,9 @@ public enum SkillSource { /** The {@code custom} variant. */ CUSTOM("custom"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code sdk} variant. */ + SDK("sdk"); private final String value; SkillSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java index a020c89ecf..5e1f6fd90e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java @@ -24,12 +24,14 @@ public record SkillsInvokedSkill( /** Unique identifier for the skill */ @JsonProperty("name") String name, - /** Path to the SKILL.md file */ + /** Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity */ @JsonProperty("path") String path, /** Full content of the skill file */ @JsonProperty("content") String content, /** Tools that should be auto-approved when this skill is active, captured at invocation time */ @JsonProperty("allowedTools") List allowedTools, + /** Whether model invocation was disabled when this skill was invoked */ + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, /** Turn number when the skill was invoked */ @JsonProperty("invokedAtTurn") Long invokedAtTurn ) { diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 39863e29a3..bfa5c8a0c6 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-2", + "@github/copilot": "^1.0.83-3", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -658,8 +658,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-2", - "integrity": "sha512-ntRvwGdZbZJjOvdV3LVBQ7z69W19DO4MWe/dh6pP8kYeumbzC4udVj7mCtoSwBOs0hwXirhXY+BZwqSJUp5FLQ==", + "version": "1.0.83-3", + "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -668,19 +668,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-2", - "@github/copilot-darwin-x64": "1.0.83-2", - "@github/copilot-linux-arm64": "1.0.83-2", - "@github/copilot-linux-x64": "1.0.83-2", - "@github/copilot-linuxmusl-arm64": "1.0.83-2", - "@github/copilot-linuxmusl-x64": "1.0.83-2", - "@github/copilot-win32-arm64": "1.0.83-2", - "@github/copilot-win32-x64": "1.0.83-2" + "@github/copilot-darwin-arm64": "1.0.83-3", + "@github/copilot-darwin-x64": "1.0.83-3", + "@github/copilot-linux-arm64": "1.0.83-3", + "@github/copilot-linux-x64": "1.0.83-3", + "@github/copilot-linuxmusl-arm64": "1.0.83-3", + "@github/copilot-linuxmusl-x64": "1.0.83-3", + "@github/copilot-win32-arm64": "1.0.83-3", + "@github/copilot-win32-x64": "1.0.83-3" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-2", - "integrity": "sha512-RBjF/zTJe+gp9PsthYtIHF36+y3A3Zv3w/2FAa9nHF6ople1+lYoQ0OC1z6tWQSpBx3n/fSkNt0ebMmfhWlQyw==", + "version": "1.0.83-3", + "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==", "cpu": [ "arm64" ], @@ -694,8 +694,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-2", - "integrity": "sha512-GzaarluCiHUA4yrYxWIFEklwPLTYjxGvLyYjI0wW5IPseT7GLFjN7AkDGRU4Ny/mxTj1MbUEsJAWVtiNy5EUHg==", + "version": "1.0.83-3", + "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==", "cpu": [ "x64" ], @@ -709,8 +709,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-2", - "integrity": "sha512-iGD5Pc7vnzrAjqTK/BrI8MDtMqXyxUe7P6u8TiGt+4DzKe3v2myT6eFQ+z/JJOHNpiu6LUpTDzwS8WttZqqJpA==", + "version": "1.0.83-3", + "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==", "cpu": [ "arm64" ], @@ -724,8 +724,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-2", - "integrity": "sha512-jOQwz075vRWat+RJiMNtD+7vm4DQ83Dvw4iakpNkLfqWMPXlliVr/tqL+SffeSo87LnhOKobZOJOjELS27drdg==", + "version": "1.0.83-3", + "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==", "cpu": [ "x64" ], @@ -739,8 +739,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-2", - "integrity": "sha512-6/+ByEBEV/vDofrCymaKrEZ5/p+TVHQfgWu8ywvWEPALfxj/iHpAF3grM2bj7uED4C19LTSarHNCkCqdrAUzIQ==", + "version": "1.0.83-3", + "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==", "cpu": [ "arm64" ], @@ -754,8 +754,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-2", - "integrity": "sha512-8dbiPUHaDemDibLfKXDe5xublJxt6fOht4yYDk/ikmOGAVezBUwRVO27PchXQBwGjP2PVNAASkv2WB4Ubw72lA==", + "version": "1.0.83-3", + "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==", "cpu": [ "x64" ], @@ -769,8 +769,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-2", - "integrity": "sha512-ZE1iUXlJSnNIH+VoRG96emy2e7jMJm85/pcKCrgLvqhDAcQSqiqLVv3OJp2VX2IcDxToOpiaGccU8Ab9zHQqkQ==", + "version": "1.0.83-3", + "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==", "cpu": [ "arm64" ], @@ -784,8 +784,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-2", - "integrity": "sha512-qfturLon+1oaWqSaXCISL6BEf5K+ijJFl0m/OZwstWYSlIlaE2+iOz5LcRukoxm25OG1JZDHQFp1k2PY2A+LbA==", + "version": "1.0.83-3", + "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 3c2bed310d..adbc639cb0 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-2", + "@github/copilot": "^1.0.83-3", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 602facf807..09df5b1ff1 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-2", + "@github/copilot": "^1.0.83-3", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index eaa477ab0a..716f76cddc 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -18426,6 +18426,17 @@ export interface SessionOpenOptions { * Additional directories to search for skills. */ skillDirectories?: string[]; + /** + * Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default. + */ + enableSkills?: boolean; + /** + * Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler. + * + * @internal + * @experimental + */ + hasSkillProvider?: boolean; /** * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. */ @@ -19981,7 +19992,7 @@ export interface SessionUpdateOptionsParams { */ enableSessionStore?: boolean; /** - * Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + * Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. */ enableSkills?: boolean; contextTier?: OptionsUpdateContextTier; @@ -20202,6 +20213,83 @@ export interface SkillList { */ skills: Skill[]; } +/** + * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderDescriptor". + */ +/** @experimental */ +export interface SkillProviderDescriptor { + /** + * Invocation and display name. + */ + name: string; + /** + * Description used in skill catalogs without fetching content. + */ + description: string; + /** + * Whether users may invoke the skill directly. Defaults to true. + */ + userInvocable?: boolean; + /** + * Whether model invocation is disabled. Defaults to false. + */ + disableModelInvocation?: boolean; + /** + * Optional freeform argument hint used by slash-command catalogs. + */ + argumentHint?: string; +} +/** + * Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderListResult". + */ +/** @experimental */ +/** @internal */ +export interface SkillProviderListResult { + /** + * Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. + * + * @maxItems 1024 + */ + skills: SkillProviderDescriptor[]; +} +/** + * Identifies one SDK-provided skill by invocation name. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderReadRequest". + */ +/** @experimental */ +/** @internal */ +export interface SkillProviderReadRequest { + /** + * Target session identifier + */ + sessionId: string; + /** + * Invocation name of the skill to read. + */ + name: string; +} +/** + * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderReadResult". + */ +/** @experimental */ +/** @internal */ +export interface SkillProviderReadResult { + /** + * Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. + */ + markdown: string; +} /** * Skill names to mark as disabled in global configuration, replacing any previous list. * @@ -20322,7 +20410,7 @@ export interface SkillsInvokedSkill { */ name: string; /** - * Path to the SKILL.md file + * Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity */ path: string; /** @@ -20333,6 +20421,10 @@ export interface SkillsInvokedSkill { * Tools that should be auto-approved when this skill is active, captured at invocation time */ allowedTools?: string[]; + /** + * Whether model invocation was disabled when this skill was invoked + */ + disableModelInvocation?: boolean; /** * Turn number when the skill was invoked */ @@ -22939,6 +23031,19 @@ export interface SessionLimitPredictionPredictRequest { modelId?: string; clientType?: SessionLimitPredictionClientType; } +/** + * Identifies the target session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderListRequest". + */ +/** @experimental */ +export interface SkillProviderListRequest { + /** + * Target session identifier + */ + sessionId: string; +} /** * Identifies the target session. * diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 93576aabf5..7d734bf491 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -1038,7 +1038,7 @@ export type FactoryRunSettledStatus = /** The run failed, with `failureType` carrying the class when it has one. */ | "error"; /** - * Source location type (e.g., project, personal-copilot, plugin, builtin) + * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) */ export type SkillSource = /** Skill defined in the current project's skill directories. */ @@ -1054,7 +1054,9 @@ export type SkillSource = /** Skill loaded from a configured custom skill directory. */ | "custom" /** Skill bundled with the runtime. */ - | "builtin"; + | "builtin" + /** Pathless skill supplied lazily by an SDK skill provider. */ + | "sdk"; /** * Whether configured models are advisory preferences or required constraints */ @@ -6835,6 +6837,10 @@ export interface SkillInvokedData { * Description of the skill from its SKILL.md frontmatter */ description?: string; + /** + * Whether model invocation is disabled for this skill + */ + disableModelInvocation?: boolean; /** * Model identifier active when the skill was invoked, when known */ @@ -6844,7 +6850,7 @@ export interface SkillInvokedData { */ name: string; /** - * File path to the SKILL.md definition + * File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity */ path: string; /** @@ -6856,7 +6862,7 @@ export interface SkillInvokedData { */ pluginVersion?: string; /** - * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) */ source?: string; trigger?: SkillInvokedTrigger; diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index b4edc24e61..b393fb6eed 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -12725,6 +12725,114 @@ class SkillDiscoveryScope(Enum): PERSONAL_COPILOT = "personal-copilot" PROJECT = "project" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillProviderDescriptor: + """Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched + separately and lazily. + """ + description: str + """Description used in skill catalogs without fetching content.""" + + name: str + """Invocation and display name.""" + + argument_hint: str | None = None + """Optional freeform argument hint used by slash-command catalogs.""" + + disable_model_invocation: bool | None = None + """Whether model invocation is disabled. Defaults to false.""" + + user_invocable: bool | None = None + """Whether users may invoke the skill directly. Defaults to true.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillProviderDescriptor': + assert isinstance(obj, dict) + description = from_str(obj.get("description")) + name = from_str(obj.get("name")) + argument_hint = from_union([from_str, from_none], obj.get("argumentHint")) + disable_model_invocation = from_union([from_bool, from_none], obj.get("disableModelInvocation")) + user_invocable = from_union([from_bool, from_none], obj.get("userInvocable")) + return SkillProviderDescriptor(description, name, argument_hint, disable_model_invocation, user_invocable) + + def to_dict(self) -> dict: + result: dict = {} + result["description"] = from_str(self.description) + result["name"] = from_str(self.name) + if self.argument_hint is not None: + result["argumentHint"] = from_union([from_str, from_none], self.argument_hint) + if self.disable_model_invocation is not None: + result["disableModelInvocation"] = from_union([from_bool, from_none], self.disable_model_invocation) + if self.user_invocable is not None: + result["userInvocable"] = from_union([from_bool, from_none], self.user_invocable) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillProviderListRequest: + """Identifies the target session.""" + + session_id: str + """Target session identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillProviderListRequest': + assert isinstance(obj, dict) + session_id = from_str(obj.get("sessionId")) + return SkillProviderListRequest(session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["sessionId"] = from_str(self.session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _SkillProviderReadRequest: + """Identifies one SDK-provided skill by invocation name.""" + + name: str + """Invocation name of the skill to read.""" + + session_id: str + """Target session identifier""" + + @staticmethod + def from_dict(obj: Any) -> '_SkillProviderReadRequest': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + session_id = from_str(obj.get("sessionId")) + return _SkillProviderReadRequest(name, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["sessionId"] = from_str(self.session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _SkillProviderReadResult: + """Complete text-only SKILL.md content returned by an SDK session's skill provider. Related + files and assets are not supported. + """ + markdown: str + """Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit.""" + + @staticmethod + def from_dict(obj: Any) -> '_SkillProviderReadResult': + assert isinstance(obj, dict) + markdown = from_str(obj.get("markdown")) + return _SkillProviderReadResult(markdown) + + def to_dict(self) -> dict: + result: dict = {} + result["markdown"] = from_str(self.markdown) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SkillsConfigSetSkillDisabledRequest: @@ -23790,11 +23898,15 @@ class SkillsInvokedSkill: """Unique identifier for the skill""" path: str - """Path to the SKILL.md file""" - + """Path to the SKILL.md file, or an empty string for an SDK-provided skill without a + filesystem identity + """ allowed_tools: list[str] | None = None """Tools that should be auto-approved when this skill is active, captured at invocation time""" + disable_model_invocation: bool | None = None + """Whether model invocation was disabled when this skill was invoked""" + @staticmethod def from_dict(obj: Any) -> 'SkillsInvokedSkill': assert isinstance(obj, dict) @@ -23803,7 +23915,8 @@ def from_dict(obj: Any) -> 'SkillsInvokedSkill': name = from_str(obj.get("name")) path = from_str(obj.get("path")) allowed_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("allowedTools")) - return SkillsInvokedSkill(content, invoked_at_turn, name, path, allowed_tools) + disable_model_invocation = from_union([from_bool, from_none], obj.get("disableModelInvocation")) + return SkillsInvokedSkill(content, invoked_at_turn, name, path, allowed_tools, disable_model_invocation) def to_dict(self) -> dict: result: dict = {} @@ -23813,6 +23926,8 @@ def to_dict(self) -> dict: result["path"] = from_str(self.path) if self.allowed_tools is not None: result["allowedTools"] = from_union([lambda x: from_list(from_str, x), from_none], self.allowed_tools) + if self.disable_model_invocation is not None: + result["disableModelInvocation"] = from_union([from_bool, from_none], self.disable_model_invocation) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -23853,6 +23968,29 @@ def to_dict(self) -> dict: result["projectPath"] = from_union([from_str, from_none], self.project_path) return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _SkillProviderListResult: + """Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to + 1024 descriptors and 1 MiB of aggregate metadata. + """ + skills: list[SkillProviderDescriptor] + """Skill descriptors in provider order. Invocation names must be unique under + case-insensitive comparison. + """ + + @staticmethod + def from_dict(obj: Any) -> '_SkillProviderListResult': + assert isinstance(obj, dict) + skills = from_list(SkillProviderDescriptor.from_dict, obj.get("skills")) + return _SkillProviderListResult(skills) + + def to_dict(self) -> dict: + result: dict = {} + result["skills"] = from_list(lambda x: to_class(SkillProviderDescriptor, x), self.skills) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandAddTimelineEntryResult: @@ -31173,6 +31311,10 @@ class SessionOpenOptions: enable_script_safety: bool | None = None """Whether shell-script safety heuristics are enabled.""" + enable_skills: bool | None = None + """Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by + default. + """ enable_streaming: bool | None = None """Whether model responses stream as delta events.""" @@ -31203,6 +31345,14 @@ class SessionOpenOptions: feature_flags: dict[str, bool] | None = None """Feature-flag values resolved by the host.""" + # Internal: this field is an internal SDK API and is not part of the public surface. + has_skill_provider: bool | None = None + """Whether the requesting SDK session has a skill provider. The provider remains ephemeral + and is never persisted in session options or history. When enableSkills is false, it + remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw + sessions.open flows reject it because they cannot safely pre-register the callback + handler. + """ included_builtin_agents: list[str] | None = None """Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the @@ -31351,6 +31501,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': enable_managed_settings = from_union([from_bool, from_none], obj.get("enableManagedSettings")) enable_on_demand_instruction_discovery = from_union([from_bool, from_none], obj.get("enableOnDemandInstructionDiscovery")) enable_script_safety = from_union([from_bool, from_none], obj.get("enableScriptSafety")) + enable_skills = from_union([from_bool, from_none], obj.get("enableSkills")) enable_streaming = from_union([from_bool, from_none], obj.get("enableStreaming")) env_value_mode = from_union([MCPSetEnvValueModeDetails, from_none], obj.get("envValueMode")) events_log_directory = from_union([from_str, from_none], obj.get("eventsLogDirectory")) @@ -31359,6 +31510,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': excluded_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("excludedTools")) exp_assignments = obj.get("expAssignments") feature_flags = from_union([lambda x: from_dict(from_bool, x), from_none], obj.get("featureFlags")) + has_skill_provider = from_union([from_bool, from_none], obj.get("hasSkillProvider")) included_builtin_agents = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinAgents")) included_builtin_skills = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinSkills")) installed_plugins = from_union([lambda x: from_list(InstalledPlugin.from_dict, x), from_none], obj.get("installedPlugins")) @@ -31395,7 +31547,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': verbosity = from_union([Verbosity, from_none], obj.get("verbosity")) 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, additional_directories, 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_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, 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, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) + return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, 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_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_skills, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, has_skill_provider, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, 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, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) def to_dict(self) -> dict: result: dict = {} @@ -31449,6 +31601,8 @@ def to_dict(self) -> dict: result["enableOnDemandInstructionDiscovery"] = from_union([from_bool, from_none], self.enable_on_demand_instruction_discovery) if self.enable_script_safety is not None: result["enableScriptSafety"] = from_union([from_bool, from_none], self.enable_script_safety) + if self.enable_skills is not None: + result["enableSkills"] = from_union([from_bool, from_none], self.enable_skills) if self.enable_streaming is not None: result["enableStreaming"] = from_union([from_bool, from_none], self.enable_streaming) if self.env_value_mode is not None: @@ -31465,6 +31619,8 @@ def to_dict(self) -> dict: result["expAssignments"] = self.exp_assignments if self.feature_flags is not None: result["featureFlags"] = from_union([lambda x: from_dict(from_bool, x), from_none], self.feature_flags) + if self.has_skill_provider is not None: + result["hasSkillProvider"] = from_union([from_bool, from_none], self.has_skill_provider) if self.included_builtin_agents is not None: result["includedBuiltinAgents"] = from_union([lambda x: from_list(from_str, x), from_none], self.included_builtin_agents) if self.included_builtin_skills is not None: @@ -31612,8 +31768,9 @@ class SessionUpdateOptionsParams: """Whether to enable cross-session store writes and reads.""" enable_skills: bool | None = None - """Whether to enable skill directory scanning and loading. Falls back to - enableConfigDiscovery when unset. + """Whether skill loading is enabled. Explicit false disables every source, including a bound + SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, + creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. """ enable_streaming: bool | None = None """Whether to stream model responses.""" @@ -36350,6 +36507,11 @@ class RPC: skill_discovery_path_list: SkillDiscoveryPathList skill_discovery_scope: SkillDiscoveryScope skill_list: SkillList + skill_provider_descriptor: SkillProviderDescriptor + skill_provider_list_request: SkillProviderListRequest + skill_provider_list_result: _SkillProviderListResult + skill_provider_read_request: _SkillProviderReadRequest + skill_provider_read_result: _SkillProviderReadResult skills_config_set_disabled_skills_request: SkillsConfigSetDisabledSkillsRequest skills_config_set_skill_disabled_request: SkillsConfigSetSkillDisabledRequest skills_disable_request: SkillsDisableRequest @@ -37539,6 +37701,11 @@ def from_dict(obj: Any) -> 'RPC': skill_discovery_path_list = SkillDiscoveryPathList.from_dict(obj.get("SkillDiscoveryPathList")) skill_discovery_scope = SkillDiscoveryScope(obj.get("SkillDiscoveryScope")) skill_list = SkillList.from_dict(obj.get("SkillList")) + skill_provider_descriptor = SkillProviderDescriptor.from_dict(obj.get("SkillProviderDescriptor")) + skill_provider_list_request = SkillProviderListRequest.from_dict(obj.get("SkillProviderListRequest")) + skill_provider_list_result = _SkillProviderListResult.from_dict(obj.get("SkillProviderListResult")) + skill_provider_read_request = _SkillProviderReadRequest.from_dict(obj.get("SkillProviderReadRequest")) + skill_provider_read_result = _SkillProviderReadResult.from_dict(obj.get("SkillProviderReadResult")) skills_config_set_disabled_skills_request = SkillsConfigSetDisabledSkillsRequest.from_dict(obj.get("SkillsConfigSetDisabledSkillsRequest")) skills_config_set_skill_disabled_request = SkillsConfigSetSkillDisabledRequest.from_dict(obj.get("SkillsConfigSetSkillDisabledRequest")) skills_disable_request = SkillsDisableRequest.from_dict(obj.get("SkillsDisableRequest")) @@ -37707,7 +37874,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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_prune_old_request, sessions_read_persisted_events_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, 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, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, 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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_prune_old_request, sessions_read_persisted_events_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, 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, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -38728,6 +38895,11 @@ def to_dict(self) -> dict: result["SkillDiscoveryPathList"] = to_class(SkillDiscoveryPathList, self.skill_discovery_path_list) result["SkillDiscoveryScope"] = to_enum(SkillDiscoveryScope, self.skill_discovery_scope) result["SkillList"] = to_class(SkillList, self.skill_list) + result["SkillProviderDescriptor"] = to_class(SkillProviderDescriptor, self.skill_provider_descriptor) + result["SkillProviderListRequest"] = to_class(SkillProviderListRequest, self.skill_provider_list_request) + result["SkillProviderListResult"] = to_class(_SkillProviderListResult, self.skill_provider_list_result) + result["SkillProviderReadRequest"] = to_class(_SkillProviderReadRequest, self.skill_provider_read_request) + result["SkillProviderReadResult"] = to_class(_SkillProviderReadResult, self.skill_provider_read_result) result["SkillsConfigSetDisabledSkillsRequest"] = to_class(SkillsConfigSetDisabledSkillsRequest, self.skills_config_set_disabled_skills_request) result["SkillsConfigSetSkillDisabledRequest"] = to_class(SkillsConfigSetSkillDisabledRequest, self.skills_config_set_skill_disabled_request) result["SkillsDisableRequest"] = to_class(SkillsDisableRequest, self.skills_disable_request) @@ -43367,6 +43539,8 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SkillDiscoveryPathList", "SkillDiscoveryScope", "SkillList", + "SkillProviderDescriptor", + "SkillProviderListRequest", "SkillsApi", "SkillsConfigSetDisabledSkillsRequest", "SkillsConfigSetSkillDisabledRequest", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index e3727eb24e..51e719bcf9 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -9410,6 +9410,7 @@ class SkillInvokedData: path: str allowed_tools: list[str] | None = None description: str | None = None + disable_model_invocation: bool | None = None model: str | None = None plugin_name: str | None = None plugin_version: str | None = None @@ -9424,6 +9425,7 @@ def from_dict(obj: Any) -> "SkillInvokedData": path = from_str(obj.get("path")) allowed_tools = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("allowedTools")) description = from_union([from_none, from_str], obj.get("description")) + disable_model_invocation = from_union([from_none, from_bool], obj.get("disableModelInvocation")) model = from_union([from_none, from_str], obj.get("model")) plugin_name = from_union([from_none, from_str], obj.get("pluginName")) plugin_version = from_union([from_none, from_str], obj.get("pluginVersion")) @@ -9435,6 +9437,7 @@ def from_dict(obj: Any) -> "SkillInvokedData": path=path, allowed_tools=allowed_tools, description=description, + disable_model_invocation=disable_model_invocation, model=model, plugin_name=plugin_name, plugin_version=plugin_version, @@ -9451,6 +9454,8 @@ def to_dict(self) -> dict: result["allowedTools"] = from_union([from_none, lambda x: from_list(from_str, x)], self.allowed_tools) if self.description is not None: result["description"] = from_union([from_none, from_str], self.description) + if self.disable_model_invocation is not None: + result["disableModelInvocation"] = from_union([from_none, from_bool], self.disable_model_invocation) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) if self.plugin_name is not None: @@ -12513,7 +12518,7 @@ class SkillInvokedTrigger(Enum): class SkillSource(Enum): - "Source location type (e.g., project, personal-copilot, plugin, builtin)" + "Source location type (e.g., project, personal-copilot, plugin, builtin, sdk)" # Skill defined in the current project's skill directories. PROJECT = "project" # Skill discovered from a parent directory in the current workspace tree. @@ -12528,6 +12533,8 @@ class SkillSource(Enum): CUSTOM = "custom" # Skill bundled with the runtime. BUILTIN = "builtin" + # Pathless skill supplied lazily by an SDK skill provider. + SDK = "sdk" class SystemMessageRole(Enum): diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 3eff4e1524..68d8b7d054 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -741,6 +741,10 @@ pub mod rpc_methods { pub const SESSION_SCHEDULE_REARMSELFPACED: &str = "session.schedule.rearmSelfPaced"; /// `session.schedule.stop` pub const SESSION_SCHEDULE_STOP: &str = "session.schedule.stop"; + /// `skillProvider.list` + pub const SKILLPROVIDER_LIST: &str = "skillProvider.list"; + /// `skillProvider.read` + pub const SKILLPROVIDER_READ: &str = "skillProvider.read"; /// `providerToken.getToken` pub const PROVIDERTOKEN_GETTOKEN: &str = "providerToken.getToken"; /// `factory.execute` @@ -16878,6 +16882,9 @@ pub struct SessionOpenOptions { /// Whether shell-script safety heuristics are enabled. #[serde(skip_serializing_if = "Option::is_none")] pub enable_script_safety: Option, + /// Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_skills: Option, /// Whether model responses stream as delta events. #[serde(skip_serializing_if = "Option::is_none")] pub enable_streaming: Option, @@ -16903,6 +16910,17 @@ pub struct SessionOpenOptions { /// Feature-flag values resolved by the host. #[serde(skip_serializing_if = "Option::is_none")] pub feature_flags: Option>, + /// Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) has_skill_provider: Option, /// Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the same name remain available. #[serde(skip_serializing_if = "Option::is_none")] pub included_builtin_agents: Option>, @@ -18348,7 +18366,7 @@ pub struct SessionUpdateOptionsParams { /// Whether to enable cross-session store writes and reads. #[serde(skip_serializing_if = "Option::is_none")] pub enable_session_store: Option, - /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, /// Whether to stream model responses. @@ -18695,6 +18713,79 @@ pub struct SkillList { pub skills: Vec, } +/// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. +/// +///
+/// +/// **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 SkillProviderDescriptor { + /// Optional freeform argument hint used by slash-command catalogs. + #[serde(skip_serializing_if = "Option::is_none")] + pub argument_hint: Option, + /// Description used in skill catalogs without fetching content. + pub description: String, + /// Whether model invocation is disabled. Defaults to false. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, + /// Invocation and display name. + pub name: String, + /// Whether users may invoke the skill directly. Defaults to true. + #[serde(skip_serializing_if = "Option::is_none")] + pub user_invocable: Option, +} + +/// Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata. +/// +///
+/// +/// **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(crate) struct SkillProviderListResult { + /// Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. + pub skills: Vec, +} + +/// Identifies one SDK-provided skill by invocation name. +/// +///
+/// +/// **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(crate) struct SkillProviderReadRequest { + /// Target session identifier + pub session_id: SessionId, + /// Invocation name of the skill to read. + pub name: String, +} + +/// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. +/// +///
+/// +/// **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(crate) struct SkillProviderReadResult { + /// Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. + pub markdown: String, +} + /// Skill names to mark as disabled in global configuration, replacing any previous list. /// ///
@@ -18814,11 +18905,14 @@ pub struct SkillsInvokedSkill { pub allowed_tools: Option>, /// Full content of the skill file pub content: String, + /// Whether model invocation was disabled when this skill was invoked + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, /// Turn number when the skill was invoked pub invoked_at_turn: i64, /// Unique identifier for the skill pub name: String, - /// Path to the SKILL.md file + /// Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity pub path: String, } @@ -27167,6 +27261,21 @@ pub struct SessionScheduleStopResult { pub entry: Option, } +/// Identifies the target session. +/// +///
+/// +/// **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 SkillProviderListParams { + /// Target session identifier + pub session_id: SessionId, +} + /// A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer ` on the outbound request and does no caching; the SDK consumer owns token caching and refresh. /// ///
diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 1bf9b75903..eb63f11438 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -4034,12 +4034,15 @@ pub struct SkillInvokedData { /// Description of the skill from its SKILL.md frontmatter #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + /// Whether model invocation is disabled for this skill + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, /// Model identifier active when the skill was invoked, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// Name of the invoked skill pub name: String, - /// File path to the SKILL.md definition + /// File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity pub path: String, /// Name of the plugin this skill originated from, when applicable #[serde(skip_serializing_if = "Option::is_none")] @@ -4047,7 +4050,7 @@ pub struct SkillInvokedData { /// Version of the plugin this skill originated from, when applicable #[serde(skip_serializing_if = "Option::is_none")] pub plugin_version: Option, - /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// What triggered the skill invocation: `user-invoked` (explicit user action, such as via a slash command or UI affordance), `agent-invoked` (agent requested the skill), or `context-load` (loaded as part of another context, such as preloading skills configured on a custom agent or subagent) @@ -6134,7 +6137,7 @@ pub struct SkillsLoadedSkill { /// Absolute path to the skill file, if available #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, - /// Source location type (e.g., project, personal-copilot, plugin, builtin) + /// Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk) pub source: SkillSource, /// Whether the skill can be invoked by the user as a slash command pub user_invocable: bool, @@ -8513,7 +8516,7 @@ pub enum FactoryRunSettledStatus { Unknown, } -/// Source location type (e.g., project, personal-copilot, plugin, builtin) +/// Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SkillSource { /// Skill defined in the current project's skill directories. @@ -8537,6 +8540,9 @@ pub enum SkillSource { /// Skill bundled with the runtime. #[serde(rename = "builtin")] Builtin, + /// Pathless skill supplied lazily by an SDK skill provider. + #[serde(rename = "sdk")] + Sdk, /// Unknown variant for forward compatibility. #[default] #[serde(other)] diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 8cf978665f..288ec1db3c 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.83-2", + "@github/copilot": "^1.0.83-3", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -472,8 +472,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-2", - "integrity": "sha512-ntRvwGdZbZJjOvdV3LVBQ7z69W19DO4MWe/dh6pP8kYeumbzC4udVj7mCtoSwBOs0hwXirhXY+BZwqSJUp5FLQ==", + "version": "1.0.83-3", + "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -483,19 +483,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-2", - "@github/copilot-darwin-x64": "1.0.83-2", - "@github/copilot-linux-arm64": "1.0.83-2", - "@github/copilot-linux-x64": "1.0.83-2", - "@github/copilot-linuxmusl-arm64": "1.0.83-2", - "@github/copilot-linuxmusl-x64": "1.0.83-2", - "@github/copilot-win32-arm64": "1.0.83-2", - "@github/copilot-win32-x64": "1.0.83-2" + "@github/copilot-darwin-arm64": "1.0.83-3", + "@github/copilot-darwin-x64": "1.0.83-3", + "@github/copilot-linux-arm64": "1.0.83-3", + "@github/copilot-linux-x64": "1.0.83-3", + "@github/copilot-linuxmusl-arm64": "1.0.83-3", + "@github/copilot-linuxmusl-x64": "1.0.83-3", + "@github/copilot-win32-arm64": "1.0.83-3", + "@github/copilot-win32-x64": "1.0.83-3" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-2", - "integrity": "sha512-RBjF/zTJe+gp9PsthYtIHF36+y3A3Zv3w/2FAa9nHF6ople1+lYoQ0OC1z6tWQSpBx3n/fSkNt0ebMmfhWlQyw==", + "version": "1.0.83-3", + "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==", "cpu": [ "arm64" ], @@ -510,8 +510,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-2", - "integrity": "sha512-GzaarluCiHUA4yrYxWIFEklwPLTYjxGvLyYjI0wW5IPseT7GLFjN7AkDGRU4Ny/mxTj1MbUEsJAWVtiNy5EUHg==", + "version": "1.0.83-3", + "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==", "cpu": [ "x64" ], @@ -526,8 +526,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-2", - "integrity": "sha512-iGD5Pc7vnzrAjqTK/BrI8MDtMqXyxUe7P6u8TiGt+4DzKe3v2myT6eFQ+z/JJOHNpiu6LUpTDzwS8WttZqqJpA==", + "version": "1.0.83-3", + "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==", "cpu": [ "arm64" ], @@ -542,8 +542,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-2", - "integrity": "sha512-jOQwz075vRWat+RJiMNtD+7vm4DQ83Dvw4iakpNkLfqWMPXlliVr/tqL+SffeSo87LnhOKobZOJOjELS27drdg==", + "version": "1.0.83-3", + "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==", "cpu": [ "x64" ], @@ -558,8 +558,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-2", - "integrity": "sha512-6/+ByEBEV/vDofrCymaKrEZ5/p+TVHQfgWu8ywvWEPALfxj/iHpAF3grM2bj7uED4C19LTSarHNCkCqdrAUzIQ==", + "version": "1.0.83-3", + "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==", "cpu": [ "arm64" ], @@ -574,8 +574,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-2", - "integrity": "sha512-8dbiPUHaDemDibLfKXDe5xublJxt6fOht4yYDk/ikmOGAVezBUwRVO27PchXQBwGjP2PVNAASkv2WB4Ubw72lA==", + "version": "1.0.83-3", + "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==", "cpu": [ "x64" ], @@ -590,8 +590,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-2", - "integrity": "sha512-ZE1iUXlJSnNIH+VoRG96emy2e7jMJm85/pcKCrgLvqhDAcQSqiqLVv3OJp2VX2IcDxToOpiaGccU8Ab9zHQqkQ==", + "version": "1.0.83-3", + "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==", "cpu": [ "arm64" ], @@ -606,8 +606,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-2", - "integrity": "sha512-qfturLon+1oaWqSaXCISL6BEf5K+ijJFl0m/OZwstWYSlIlaE2+iOz5LcRukoxm25OG1JZDHQFp1k2PY2A+LbA==", + "version": "1.0.83-3", + "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index 91aaff849a..7b04900614 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.83-2", + "@github/copilot": "^1.0.83-3", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", From cc6d9a909cff8a62c8a5b51927c09001acae08b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:44:56 +0000 Subject: [PATCH 34/60] build(deps-dev): bump js-yaml from 4.2.0 to 4.3.2 in /nodejs (#2487) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.2. - [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.2/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.2) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.3.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- nodejs/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index bfa5c8a0c6..2dd065c1bb 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -2549,8 +2549,8 @@ "license": "BSD-3-Clause" }, "node_modules/js-yaml": { - "version": "4.2.0", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.2", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { From ec1c6f4b63684a70415f32c13efe336399773a05 Mon Sep 17 00:00:00 2001 From: Devraj Mehta Date: Thu, 3 Sep 2026 08:31:21 -0400 Subject: [PATCH 35/60] Use Copilot CLI releases for Node runtime (#2463) * Use Copilot CLI releases for Node runtime Remove the Node SDK dependency on @github/copilot, pin the CLI release and checksums in the repository, and acquire verified platform runtime assets from GitHub Releases. Update packaging, code generation, tests, and cross-SDK version consumers to use the shared release pin. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Address CLI release runtime review findings Prepare the pinned runtime from Python, .NET, Go, and Rust E2E harnesses; preserve internal npm canaries; and centralize trusted release hashes for Node, Java, and Rust packaging. Update Rust pin validation and bundled runtime contents accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Package CLI runtimes by platform Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Use release checksum manifests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Fix cross-platform release runtime CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Address release runtime code scanning Stream Java release archives directly into tar and document generated canary condition branches for CodeQL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Fix release runtime CI regressions Preserve the npm runtime layout, use the legacy CLI for standalone extension tests, align the Python account test with current auth behavior, and remove a Go shutdown race. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Fix runtime packaging integration gaps Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Report early CLI exits consistently Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Retrigger PR checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Run npm packaging through Node Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Resolve runtime path analysis findings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Address final runtime review findings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Harden release packaging metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Avoid unsafe runtime path combination Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Read pinned CLI version in all Rust jobs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Isolate legacy extension tests on macOS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Address runtime packaging review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Split hanging macOS .NET test shard Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Timeout stalled CLI release downloads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Isolate macOS permission test fixtures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Isolate macOS RPC test fixtures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Cancel blocked tools during resume tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Use cross-target cancellation wait Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Stabilize cross-platform E2E cleanup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Finish CLI runtime rebase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Synchronize Go session event shutdown Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Stabilize event fidelity ordering checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Wait for disconnected tools to unregister Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Update rebased CLI release pin Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 --------- Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 --- .github/actions/setup-copilot/action.yml | 11 +- .github/copilot-instructions.md | 4 +- .github/workflows/dotnet-sdk-tests.yml | 118 +- .github/workflows/java-sdk-tests.yml | 7 +- .github/workflows/nodejs-sdk-tests.yml | 11 + .github/workflows/publish.yml | 60 +- .github/workflows/required-checks.yml | 11 +- .github/workflows/rust-sdk-tests.yml | 20 +- .github/workflows/sdk-canary.yml | 819 ++--- .../workflows/update-copilot-dependency.yml | 29 +- dotnet/src/Client.cs | 10 +- dotnet/src/GitHub.Copilot.SDK.csproj | 4 +- dotnet/src/build/GitHub.Copilot.SDK.targets | 4 +- dotnet/test/E2E/EventFidelityE2ETests.cs | 9 +- dotnet/test/E2E/MultiClientE2ETests.cs | 3 + dotnet/test/E2E/PendingWorkResumeE2ETests.cs | 24 +- .../test/E2E/RpcExtensionsLoadedE2ETests.cs | 63 +- dotnet/test/Harness/E2ETestContext.cs | 92 +- dotnet/test/Unit/MSBuildTargetsTests.cs | 6 + go/README.md | 5 +- go/client.go | 6 +- go/client_test.go | 45 +- go/cmd/bundler/main.go | 56 +- go/cmd/bundler/main_test.go | 9 +- go/internal/e2e/event_fidelity_e2e_test.go | 4 +- go/internal/e2e/testharness/context.go | 22 +- go/samples/chat.go | 7 +- go/samples/internal/sampleutil/runtime.go | 48 + go/samples/manual_tool_resume/main.go | 7 +- go/session.go | 32 +- go/session_test.go | 29 +- go/test.sh | 9 +- java/README.md | 4 +- java/copilot-native/pom.xml | 16 +- java/copilot-native/scripts/fetch-native.mjs | 115 +- .../scripts/fetch-native.test.mjs | 63 +- .../scripts/validate-native-artifact.mjs | 15 +- .../scripts/validate-native-artifact.test.mjs | 22 +- .../adr/adr-007-native-bundling-strategy.md | 2 +- java/sdk/pom.xml | 35 +- .../com/github/copilot/E2ETestContext.java | 52 +- .../java/com/github/copilot/TestUtil.java | 77 +- nodejs/README.md | 33 +- nodejs/package-lock.json | 351 +-- nodejs/package.json | 9 +- nodejs/samples/package-lock.json | 2750 ++++++++++++++++- nodejs/scripts/package-sdk.ts | 73 + nodejs/scripts/prepare-runtime.ts | 16 + nodejs/scripts/releaseArtifacts.ts | 233 ++ nodejs/scripts/set-cli-version.js | 71 + nodejs/scripts/verify-release-packages.ts | 100 + nodejs/src/cliVersion.ts | 3 + nodejs/src/client.ts | 112 +- nodejs/src/runtimeArtifacts.ts | 144 +- nodejs/test/e2e/event_fidelity.e2e.test.ts | 4 +- .../test/e2e/extension_env_access.e2e.test.ts | 6 +- nodejs/test/e2e/factory.e2e.test.ts | 2 +- nodejs/test/e2e/harness/sdkTestContext.ts | 25 +- nodejs/test/e2e/ui_elicitation.e2e.test.ts | 4 +- nodejs/test/runtimeArtifacts.test.ts | 255 +- python/copilot/_ffi_runtime_host.py | 7 +- python/copilot/_jsonrpc.py | 30 +- python/copilot/client.py | 16 +- python/e2e/conftest.py | 6 +- python/e2e/test_event_fidelity_e2e.py | 2 +- python/e2e/test_rpc_server_misc_e2e.py | 22 +- python/e2e/testharness/context.py | 86 +- python/scripts/inject-cli-version.mjs | 18 +- python/test_cli_download.py | 11 +- python/test_e2e_harness_cli_path.py | 143 +- rust/Cargo.toml | 1 - rust/README.md | 6 +- rust/build/in_process.rs | 213 +- rust/build/out_of_process.rs | 105 +- rust/scripts/snapshot-bundled-cli-version.sh | 44 +- .../snapshot-bundled-in-process-version.sh | 41 +- rust/tests/cli_resolution_test.rs | 36 +- rust/tests/e2e/support.rs | 59 +- scripts/codegen/utils.ts | 54 +- test/harness/package-lock.json | 421 +-- test/harness/package.json | 1 - 81 files changed, 5245 insertions(+), 2253 deletions(-) create mode 100644 go/samples/internal/sampleutil/runtime.go create mode 100644 nodejs/scripts/package-sdk.ts create mode 100644 nodejs/scripts/prepare-runtime.ts create mode 100644 nodejs/scripts/releaseArtifacts.ts create mode 100644 nodejs/scripts/set-cli-version.js create mode 100644 nodejs/scripts/verify-release-packages.ts create mode 100644 nodejs/src/cliVersion.ts diff --git a/.github/actions/setup-copilot/action.yml b/.github/actions/setup-copilot/action.yml index 3769bc3751..506f472ca2 100644 --- a/.github/actions/setup-copilot/action.yml +++ b/.github/actions/setup-copilot/action.yml @@ -23,16 +23,15 @@ runs: - name: Set CLI path id: cli-path run: | - # 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. - cli_path=$(ls "$(pwd)"/nodejs/node_modules/@github/copilot-*/index.js 2>/dev/null | head -n1) + cli_path=$(npm --prefix "$(pwd)/nodejs" run --silent prepare:runtime -- --print-path) if [ -z "$cli_path" ]; then - echo "Could not find @github/copilot platform package (index.js) under nodejs/node_modules" >&2 + echo "Could not prepare the Copilot CLI runtime" >&2 exit 1 fi echo "path=$cli_path" >> $GITHUB_OUTPUT shell: bash - name: Verify CLI works - run: node ${{ steps.cli-path.outputs.path }} --version + run: | + legacy_cli=$(npm --prefix "$(pwd)/nodejs" run --silent prepare:runtime -- --print-legacy-path) + node "$legacy_cli" --version shell: bash diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a9bc22d0ec..476f4e5689 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -47,7 +47,7 @@ - Tools: each SDK has helper APIs to expose functions as tools; prefer the language's `DefineTool`/`@define_tool`/`CopilotTool.DefineTool` patterns (see language READMEs). - Infinite sessions are enabled by default and persist workspace state to `~/.copilot/session-state/{sessionId}`; compaction events are emitted (`session.compaction_start`, `session.compaction_complete`). See language READMEs for usage. - Streaming: when `streaming`/`Streaming=true` you receive delta events (`assistant.message_delta`, `assistant.reasoning_delta`) and final events (`assistant.message`, `assistant.reasoning`) — tests expect this behavior. -- Type generation is centralized in `nodejs/scripts/generate-session-types.ts` and requires the `@github/copilot` schema to be present (often via `npm link` or installed package). +- Type generation is centralized in `scripts/codegen/` and downloads schemas from the pinned `github/copilot-cli` release. - Java code style: 4-space indent (Spotless + Eclipse formatter), fluent setter pattern for config classes, Javadoc required on public APIs (enforced by Checkstyle, except `json`/`events` packages). - Java handlers return `CompletableFuture` (the Java equivalent of C# `async/await`). When porting from .NET: convert properties → getters/fluent setters, use Jackson (`ObjectMapper`, `@JsonProperty`) for serialization. @@ -64,7 +64,7 @@ - SDK code: `nodejs/src`, `python/copilot`, `go`, `dotnet/src`, `rust/src`, `java/sdk/src/main/java` - Unit tests: `nodejs/test`, `python/*`, `go/*`, `dotnet/test`, `rust/tests`, `java/sdk/src/test/java` - E2E tests: `*/e2e/` folders that use the shared replay proxy and `test/snapshots/`, `java/sdk/src/test/java/**/e2e/` -- Generated types: update schema in `@github/copilot` then run `cd nodejs && npm run generate:session-types` and commit generated files in `src/generated` or language generated location. Java generated types: `java/sdk/src/generated/java` +- Generated types: update the pinned Copilot CLI version, run `cd nodejs && npm run generate`, and commit generated files in each language's generated location. Java generated types: `java/sdk/src/generated/java` ## Boundaries — files you must NOT hand-edit ⛔ diff --git a/.github/workflows/dotnet-sdk-tests.yml b/.github/workflows/dotnet-sdk-tests.yml index 0acf807f0a..9b7216ad20 100644 --- a/.github/workflows/dotnet-sdk-tests.yml +++ b/.github/workflows/dotnet-sdk-tests.yml @@ -104,43 +104,65 @@ jobs: - os: macos-latest transport: default backend: capi - shard: "2b1a-permission-provider" + shard: "2b-pending" - os: macos-latest transport: default backend: capi - shard: "2b1a-rewind" + shard: "2b-permission" - os: macos-latest transport: default backend: capi - shard: "2b1a-rpc-a" + shard: "2b-auth" - os: macos-latest transport: default backend: capi - shard: "2b1a-rpc-e" + shard: "2b-hooks" - os: macos-latest transport: default backend: capi - shard: "2b1b-mcp-and-skills" + shard: "2b-unit-p" - os: macos-latest transport: default backend: capi - shard: "2b1b-mcp-config" + shard: "2b-provider" - os: macos-latest transport: default backend: capi - shard: "2b1b-mcp-lifecycle" + shard: "2b-rpc-additional" - os: macos-latest transport: default backend: capi - shard: "2b1b-queue" + shard: "2b-rpc-agent" - os: macos-latest transport: default backend: capi - shard: "2b1b-remote" + shard: "2b-rpc-event-log" - os: macos-latest transport: default backend: capi - shard: "2b2" + shard: "2b-rpc-event-effects" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-mcp-skills" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-mcp-config" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-mcp-lifecycle" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-q-z" + # Standalone extension tests require the legacy JavaScript entrypoint. + # Keep its subprocess tree isolated from the rest of the O/P/R shard. + - os: macos-latest + transport: default + backend: capi + shard: extensions - os: macos-latest transport: default backend: capi @@ -223,7 +245,6 @@ jobs: ) filter="$DOTNET_TEST_FILTER" - individual_filters=() if [[ "$DOTNET_TEST_SHARD" != "full" ]]; then case "$DOTNET_TEST_SHARD" in 1) @@ -240,54 +261,65 @@ jobs: initials=(B E F G) shard_filter="" ;; - 2b1a-permission-provider) + 2b-pending) initials=() - shard_filter="" - individual_filters=( - "FullyQualifiedName~GitHub.Copilot.Test.E2E.PendingWorkResumeE2ETests" - "FullyQualifiedName~GitHub.Copilot.Test.E2E.PerSessionAuthE2ETests" - "FullyQualifiedName~GitHub.Copilot.Test.E2E.PermissionE2ETests" - "FullyQualifiedName~GitHub.Copilot.Test.E2E.PreMcpToolCallHookE2ETests" - "FullyQualifiedName~GitHub.Copilot.Test.E2E.ProviderEndpointE2ETests" - "FullyQualifiedName~GitHub.Copilot.Test.Unit.PermissionHandlerTests" - "FullyQualifiedName~GitHub.Copilot.Test.Unit.PublicDtoTests" - ) + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PendingWorkResumeE2ETests" ;; - 2b1a-rewind) + 2b-permission) initials=() - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.Rewind" + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PermissionE2ETests" ;; - 2b1a-rpc-a) + 2b-auth) initials=() - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcA" + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PerSessionAuthE2ETests" ;; - 2b1a-rpc-e) + 2b-hooks) initials=() - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcE" + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PreMcpToolCallHookE2ETests" ;; - 2b1b-mcp-and-skills) + 2b-unit-p) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.Unit.P" + ;; + 2b-provider) + initials=(O) + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.ProviderEndpointE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RewindE2ETests|FullyQualifiedName~GitHub.Copilot.Test.Unit.R" + ;; + 2b-rpc-additional) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcAdditionalEdgeCasesE2ETests" + ;; + 2b-rpc-agent) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcAgentE2ETests" + ;; + 2b-rpc-event-log) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcEventLogE2ETests" + ;; + 2b-rpc-event-effects) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcEventSideEffectsE2ETests" + ;; + 2b-rpc-mcp-skills) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpAndSkillsE2ETests" ;; - 2b1b-mcp-config) + 2b-rpc-mcp-config) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpConfigE2ETests" ;; - 2b1b-mcp-lifecycle) + 2b-rpc-mcp-lifecycle) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpLifecycleE2ETests" ;; - 2b1b-queue) + 2b-rpc-q-z) initials=() - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcQ" + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcQueueE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcRemoteE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcScheduleE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerMiscE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerPluginsE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerRemoteControlE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcSessionStateE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcSessionStateExtrasE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellAndFleetE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellEdgeCaseE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellUserRequestedE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcTasksAndHandlersE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcUiEphemeralQueryE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcWorkspaceCheckpointsE2ETests" ;; - 2b1b-remote) + extensions) initials=() - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcR" - ;; - 2b2) - initials=() - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcS|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcT|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcU|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcW|FullyQualifiedName~GitHub.Copilot.Test.Unit.R" + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcExtensionsLoadedE2ETests" ;; 2c) initials=(T V X Z) @@ -295,14 +327,6 @@ jobs: ;; esac - if (( ${#individual_filters[@]} > 0 )); then - for individual_filter in "${individual_filters[@]}"; do - combined_filter="${filter:+(${filter})&}(${individual_filter})" - dotnet test test/GitHub.Copilot.SDK.Test.csproj "${args[@]}" --filter "$combined_filter" - done - exit 0 - fi - for namespace in E2E Unit; do for initial in "${initials[@]}"; do clause="FullyQualifiedName~GitHub.Copilot.Test.${namespace}.${initial}" diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index 88185deb14..0b6b62a0d9 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -458,7 +458,12 @@ jobs: run: mvn javadoc:javadoc -q - name: Verify CLI works - run: node ../nodejs/node_modules/@github/copilot/npm-loader.js --version + run: | + npm --prefix ../nodejs ci --ignore-scripts + cli_path=$(npm --prefix ../nodejs run --silent prepare:runtime -- --print-path) + test -x "$cli_path" + legacy_cli=$(npm --prefix ../nodejs run --silent prepare:runtime -- --print-legacy-path) + node "$legacy_cli" --version - name: Run spotless check if: matrix.test-jdk == '25' diff --git a/.github/workflows/nodejs-sdk-tests.yml b/.github/workflows/nodejs-sdk-tests.yml index 1738828e6b..894f3b5d0d 100644 --- a/.github/workflows/nodejs-sdk-tests.yml +++ b/.github/workflows/nodejs-sdk-tests.yml @@ -49,6 +49,12 @@ jobs: - name: Build SDK run: npm run build + - name: Build and verify release packages + if: runner.os == 'Linux' && matrix.transport == 'default' + run: | + npm run pack:release + npm run verify:release-packages + - name: Install test harness dependencies working-directory: ./test/harness run: npm ci --ignore-scripts @@ -58,6 +64,11 @@ jobs: working-directory: ./test/harness run: npm test + - name: Prepare Copilot CLI runtime + run: | + runtime_path=$(npm run --silent prepare:runtime -- --print-path) + echo "COPILOT_CLI_PATH=$runtime_path" >> "$GITHUB_ENV" + - name: Warm up PowerShell if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 98bf236900..d1961d4a97 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -107,19 +107,18 @@ jobs: - name: Build run: npm run build - name: Pack - id: pack run: | - TARBALL="$(npm pack . --json | jq -r '.[0].filename')" - if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then - echo "::error::npm pack did not produce a tarball." + npm run pack:release + TARBALL_COUNT="$(find . -maxdepth 1 -name 'github-copilot-sdk-*.tgz' | wc -l | tr -d ' ')" + if [ "$TARBALL_COUNT" -ne 9 ]; then + echo "::error::Expected nine Node.js package tarballs, found $TARBALL_COUNT." exit 1 fi - echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT" - name: Upload artifact uses: actions/upload-artifact@v7.0.0 with: name: nodejs-package - path: nodejs/${{ steps.pack.outputs.tarball }} + path: nodejs/github-copilot-sdk-*.tgz if-no-files-found: error publish-nodejs: @@ -150,12 +149,29 @@ jobs: set -euo pipefail shopt -s nullglob TARBALLS=(./dist/*.tgz) - if [ "${#TARBALLS[@]}" -ne 1 ]; then - echo "::error::Expected exactly one Node.js package tarball, found ${#TARBALLS[@]}." + if [ "${#TARBALLS[@]}" -ne 9 ]; then + echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." + exit 1 + fi + MAIN_TARBALL="" + for TARBALL in "${TARBALLS[@]}"; do + PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" + if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then + MAIN_TARBALL="$TARBALL" + continue + fi + node nodejs/scripts/npm-release.js publish \ + "$TARBALL" \ + "$DIST_TAG" \ + https://registry.npmjs.org \ + public + done + if [ -z "$MAIN_TARBALL" ]; then + echo "::error::Main @github/copilot-sdk tarball not found." exit 1 fi node nodejs/scripts/npm-release.js publish \ - "${TARBALLS[0]}" \ + "$MAIN_TARBALL" \ "$DIST_TAG" \ https://registry.npmjs.org \ public @@ -209,12 +225,29 @@ jobs: fi shopt -s nullglob TARBALLS=(./dist/*.tgz) - if [ "${#TARBALLS[@]}" -ne 1 ]; then - echo "::error::Expected exactly one Node.js package tarball, found ${#TARBALLS[@]}." + if [ "${#TARBALLS[@]}" -ne 9 ]; then + echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." + exit 1 + fi + MAIN_TARBALL="" + for TARBALL in "${TARBALLS[@]}"; do + PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" + if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then + MAIN_TARBALL="$TARBALL" + continue + fi + node nodejs/scripts/npm-release.js publish \ + "$TARBALL" \ + "$DIST_TAG" \ + "$FEED_URL" \ + azure + done + if [ -z "$MAIN_TARBALL" ]; then + echo "::error::Main @github/copilot-sdk tarball not found." exit 1 fi node nodejs/scripts/npm-release.js publish \ - "${TARBALLS[0]}" \ + "$MAIN_TARBALL" \ "$DIST_TAG" \ "$FEED_URL" \ azure @@ -327,9 +360,6 @@ jobs: node-version: "22.x" - name: Set up uv uses: astral-sh/setup-uv@v7 - - name: Install Node.js dependencies (for CLI version) - working-directory: ./nodejs - run: npm ci --ignore-scripts - name: Set version run: sed -i "s/^version = .*/version = \"${{ needs.version.outputs.version }}\"/" pyproject.toml - name: Inject CLI version diff --git a/.github/workflows/required-checks.yml b/.github/workflows/required-checks.yml index f4083dd307..0fb8acf70b 100644 --- a/.github/workflows/required-checks.yml +++ b/.github/workflows/required-checks.yml @@ -37,42 +37,41 @@ jobs: - '.github/workflows/required-checks.yml' nodejs: - '{nodejs/**,test/**,.github/workflows/nodejs-sdk-tests.yml}' - - '!nodejs/scripts/**' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' python: - - '{python/**,test/**,nodejs/package.json,.github/workflows/python-sdk-tests.yml}' + - '{python/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/python-sdk-tests.yml}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' go: - - '{go/**,test/**,nodejs/package.json,.github/workflows/go-sdk-tests.yml,.github/actions/setup-copilot/**}' + - '{go/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/go-sdk-tests.yml,.github/actions/setup-copilot/**}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' dotnet: - - '{dotnet/**,test/**,nodejs/package.json,.github/workflows/dotnet-sdk-tests.yml}' + - '{dotnet/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/dotnet-sdk-tests.yml}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' java: - - '{java/**,test/**,.github/workflows/java-sdk-tests.yml,.github/actions/setup-copilot/**,.github/actions/java-test-report/**}' + - '{java/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/java-sdk-tests.yml,.github/actions/setup-copilot/**,.github/actions/java-test-report/**}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' rust: - - '{rust/**,test/**,nodejs/package.json,.github/workflows/rust-sdk-tests.yml,.github/actions/setup-copilot/**}' + - '{rust/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/rust-sdk-tests.yml,.github/actions/setup-copilot/**}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' diff --git a/.github/workflows/rust-sdk-tests.yml b/.github/workflows/rust-sdk-tests.yml index da030ff999..828459126b 100644 --- a/.github/workflows/rust-sdk-tests.yml +++ b/.github/workflows/rust-sdk-tests.yml @@ -43,11 +43,11 @@ jobs: prefix-key: v1-rust-no-bin cache-bin: false - - name: Read pinned @github/copilot CLI version + - name: Read pinned Copilot CLI version id: cli-version working-directory: ./nodejs run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + version=$(node -p "require('./package.json').copilotCliVersion") echo "version=$version" >> "$GITHUB_OUTPUT" echo "Pinned CLI version: $version" @@ -119,11 +119,11 @@ jobs: prefix-key: v1-rust-no-bin cache-bin: false - - name: Read pinned @github/copilot CLI version + - name: Read pinned Copilot CLI version id: cli-version working-directory: ./nodejs run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + version=$(node -p "require('./package.json').copilotCliVersion") echo "version=$version" >> "$GITHUB_OUTPUT" echo "Pinned CLI version: $version" @@ -168,11 +168,11 @@ jobs: prefix-key: v1-rust-no-bin cache-bin: false - - name: Read pinned @github/copilot CLI version + - name: Read pinned Copilot CLI version id: cli-version working-directory: ./nodejs run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + version=$(node -p "require('./package.json').copilotCliVersion") echo "version=$version" >> "$GITHUB_OUTPUT" echo "Pinned CLI version: $version" @@ -231,11 +231,11 @@ jobs: prefix-key: v1-rust-no-bin cache-bin: false - - name: Read pinned @github/copilot CLI version + - name: Read pinned Copilot CLI version id: cli-version working-directory: ./nodejs run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + version=$(node -p "require('./package.json').copilotCliVersion") echo "version=$version" >> "$GITHUB_OUTPUT" echo "Pinned CLI version: $version" @@ -307,11 +307,11 @@ jobs: prefix-key: v1-rust-no-bin cache-bin: false - - name: Read pinned @github/copilot CLI version + - name: Read pinned Copilot CLI version id: cli-version working-directory: ./nodejs run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + version=$(node -p "require('./package.json').copilotCliVersion") echo "version=$version" >> "$GITHUB_OUTPUT" echo "Pinned CLI version: $version" diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 95f8b1c926..7425f8e720 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -1,391 +1,428 @@ -name: "SDK Canary Test/Publish" - -# Nightly-style canary pipeline. First installs an explicit version of the -# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite -# against it to prove runtime <-> SDK compatibility. When that gate passes (and -# mode allows), publishes an SDK canary pinned to the tested runtime to the -# internal Azure Artifacts feed only (never public npm). - -env: - HUSKY: 0 - # Internal org-scoped Azure Artifacts feed — single source of truth so the - # feed name isn't repeated across steps. The SDK canary publishes here and - # (when runtime_source=internal) installs the runtime from here; it must NEVER - # reach public npm (@github/copilot-sdk is a live public package). - FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/ - # Azure DevOps resource ID used to mint an ADO access token for the feed. - ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 - -on: - workflow_dispatch: - inputs: - runtime_version: - description: "Exact @github/copilot version to test (e.g. 1.0.69 or 1.0.70-canary.)" - required: true - type: string - runtime_source: - description: "Where to install the runtime from" - required: true - type: choice - options: - - public - - internal - default: public - mode: - description: "publish (tests must pass), publish-force (publish even if tests fail), or tests-only (run gate, never publish)" - required: false - type: choice - default: publish - options: - - publish - - publish-force - - tests-only - repository_dispatch: - types: [runtime-canary] - -permissions: - contents: read - id-token: write - -# Serialize runs per ref so two overlapping canary runs can't race the feed -# publish. cancel-in-progress: false — never kill an in-flight publish. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - resolve: - name: "Resolve runtime inputs" - if: github.event.repository.fork == false - runs-on: ubuntu-latest - permissions: {} - outputs: - RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} - RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }} - PUBLISH_MODE: ${{ steps.normalize.outputs.PUBLISH_MODE }} - steps: - # Normalize whichever trigger fired into a single (RUNTIME_VERSION, - # RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step - # references. workflow_dispatch reads the human-supplied inputs; - # repository_dispatch reads client_payload and forces source=internal - # (a runtime canary only exists on the feed), defaulting mode to publish. - - name: Normalize inputs - id: normalize - env: - EVENT_NAME: ${{ github.event_name }} - INPUT_VERSION: ${{ inputs.runtime_version }} - INPUT_SOURCE: ${{ inputs.runtime_source }} - INPUT_MODE: ${{ inputs.mode }} - PAYLOAD_VERSION: ${{ github.event.client_payload.runtime_version }} - PAYLOAD_SOURCE: ${{ github.event.client_payload.runtime_source }} - PAYLOAD_MODE: ${{ github.event.client_payload.mode }} - run: | - set -euo pipefail - case "$EVENT_NAME" in - workflow_dispatch) - VERSION="$INPUT_VERSION" - SOURCE="$INPUT_SOURCE" - MODE="$INPUT_MODE" - ;; - repository_dispatch) - VERSION="$PAYLOAD_VERSION" - # A runtime canary only ever exists on the internal feed. - SOURCE="${PAYLOAD_SOURCE:-internal}" - MODE="${PAYLOAD_MODE:-publish}" - ;; - *) - echo "::error::Unsupported event '$EVENT_NAME'." - exit 1 - ;; - esac - if [ -z "$VERSION" ]; then echo "::error::Could not determine runtime version."; exit 1; fi - if [ -z "$SOURCE" ]; then SOURCE="public"; fi - case "$SOURCE" in - public|internal) ;; - *) echo "::error::Invalid runtime source '$SOURCE'. Expected one of: public, internal."; exit 1 ;; - esac - if [ -z "$MODE" ]; then MODE="publish"; fi - case "$MODE" in - publish|publish-force|tests-only) ;; - *) echo "::error::Invalid publish mode '$MODE'. Expected one of: publish, publish-force, tests-only."; exit 1 ;; - esac - echo "Resolved RUNTIME_VERSION=$VERSION RUNTIME_SOURCE=$SOURCE PUBLISH_MODE=$MODE" - echo "RUNTIME_VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "RUNTIME_SOURCE=$SOURCE" >> "$GITHUB_OUTPUT" - echo "PUBLISH_MODE=$MODE" >> "$GITHUB_OUTPUT" - - - name: Validate runtime version (semver) - env: - RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} - run: | - if [[ ! "$RUNTIME_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then - echo "::error::Invalid runtime version '$RUNTIME_VERSION'. Expected semver (e.g. 1.0.69 or 1.0.70-canary.abc123)." - exit 1 - fi - - test: - name: "E2E tests (${{ matrix.os }})" - needs: resolve - if: github.event.repository.fork == false - environment: cicd - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - env: - POWERSHELL_UPDATECHECK: Off - RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} - RUNTIME_SOURCE: ${{ needs.resolve.outputs.RUNTIME_SOURCE }} - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - uses: actions/checkout@v6.0.2 - - - uses: actions/setup-node@v6 - with: - cache: "npm" - cache-dependency-path: "./nodejs/package-lock.json" - node-version: 22 - - - name: Install SDK dependencies - run: npm ci --ignore-scripts - - - name: Install test harness dependencies - working-directory: ./test/harness - run: npm ci --ignore-scripts - - - name: Azure Login (OIDC -> id-cpd-ci) - if: env.RUNTIME_SOURCE == 'internal' - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci - tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" - allow-no-subscriptions: true - - # Route ONLY @github/* (the runtime + its 8 platform packages) to the - # internal feed via a scoped registry. All other deps (e.g. detect-libc) - # still resolve from public npm. A global --registry would break because - # detect-libc is not on the feed. - - name: Configure canary feed (.npmrc) - if: env.RUNTIME_SOURCE == 'internal' - run: | - set -euo pipefail - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - # Derive the protocol-relative auth scopes from FEED_URL so the feed - # name lives in exactly one place (the workflow-level env). - FEED_AUTH_REGISTRY="${FEED_URL#https:}" - FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - NPMRC="$(printf '%s\n' \ - "@github:registry=${FEED_URL}" \ - "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ - "${FEED_AUTH_BASE}:_authToken=${TOKEN}")" - printf '%s\n' "$NPMRC" > .npmrc - echo "Wrote scoped @github registry .npmrc to ./nodejs" - - - name: Override runtime version - run: | - set -euo pipefail - echo "Installing @github/copilot@${RUNTIME_VERSION} (source: ${RUNTIME_SOURCE})" - npm install "@github/copilot@${RUNTIME_VERSION}" --save-exact --ignore-scripts - - - name: Verify installed runtime - run: | - set -euo pipefail - node -e ' - const fs = require("fs"); - const expected = process.env.RUNTIME_VERSION; - const pkg = require("./node_modules/@github/copilot/package.json"); - if (pkg.version !== expected) { - console.error(`::error::Installed @github/copilot version ${pkg.version} does not match requested ${expected}`); - process.exit(1); - } - const dir = "./node_modules/@github"; - const entries = fs.readdirSync(dir).filter((d) => d.startsWith("copilot-")); - const plat = process.platform === "win32" ? "win32" : process.platform === "darwin" ? "darwin" : "linux"; - const arch = process.arch; - const match = entries.find((d) => d.includes(plat) && d.includes(arch)); - if (!match) { - console.error(`::error::No @github/copilot platform optional dep for ${plat}-${arch}. Present: ${entries.join(", ") || "(none)"}`); - process.exit(1); - } - const platPkg = require(`${dir}/${match}/package.json`); - if (platPkg.version !== expected) { - console.error(`::error::Platform package @github/${match} version ${platPkg.version} does not match requested ${expected}`); - process.exit(1); - } - console.log(`Verified @github/copilot@${pkg.version} with platform package @github/${match}@${platPkg.version}`); - ' - - - name: Build SDK - run: npm run build - - - name: Warm up PowerShell - if: runner.os == 'Windows' - run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - - name: Run Node.js SDK e2e tests - env: - COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - run: npm test - - publish: - name: "Publish SDK canary (internal feed)" - needs: [resolve, test] - # Publish runs only when the gate permits it. Mode governs behavior: - # - tests-only: never publish (skips this job entirely). - # - publish: publish only when the e2e gate is green (the default for both - # the human and automated triggers). - # - publish-force: publish even on a non-green gate — a human-acknowledged - # flake override, audited via the ::warning:: step below and the run actor. - # publish-force only skips the e2e *signal* — the publish job still runs the - # build (so a broken build can't publish) and enforces the feed-only guards. - if: > - !cancelled() && - github.event.repository.fork == false && - needs.resolve.result == 'success' && - needs.resolve.outputs.PUBLISH_MODE != 'tests-only' && - (needs.test.result == 'success' || - needs.resolve.outputs.PUBLISH_MODE == 'publish-force') - environment: cicd - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - env: - RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - name: Warn — publishing despite failed e2e gate (publish-force) - # always() so this audit is never skipped by prior-step status; it fires - # specifically when publish proceeded on a non-green gate via publish-force. - # Runs at the workspace root because it executes before checkout, so the - # job's default working-directory (./nodejs) does not exist yet. - if: always() && needs.test.result != 'success' && needs.resolve.outputs.PUBLISH_MODE == 'publish-force' - working-directory: ${{ github.workspace }} - run: | - echo "::warning title=e2e gate bypassed::Publishing SDK canary despite a non-passing e2e gate (test job result: ${{ needs.test.result }}) via publish-force. Triggered by '${{ github.actor }}' through '${{ github.event_name }}'. The e2e signal was bypassed; build + feed-only guards still apply." - - - uses: actions/checkout@v6.0.2 - - - uses: actions/setup-node@v6 - with: - node-version: 22 - - # Default public registry: installs build deps and the currently pinned - # runtime. Do NOT write any feed .npmrc or scoped @github:registry line - # here, or npm ci would try to fetch the runtime from the upstream-less - # feed and 404. - - name: Install SDK dependencies - run: npm ci --ignore-scripts - - - name: Compute SDK canary version - id: sdkver - env: - RUN_NUMBER: ${{ github.run_number }} - SHA: ${{ github.sha }} - run: | - set -euo pipefail - SHORT_SHA="${SHA:0:7}" - # Base the canary on the NEXT patch of the public SDK latest so canaries - # correlate with public releases: they sort ABOVE the current public - # latest and BELOW the eventual real release of that next patch (a - # prerelease of X.Y.Z always sorts below X.Y.Z), so a canary can never - # shadow the real release when it ships. - # Reuse the repo's own version helper (scripts/get-version.js) so this - # stays consistent with publish.yml: `current` returns the latest public - # dist-tag version, read-only from public npm (never the feed), then - # we bump the patch ourselves to keep strict patch+1 semantics. - PUBLIC_LATEST="$(node scripts/get-version.js current || true)" - BASE="${PUBLIC_LATEST%%-*}"; BASE="${BASE%%+*}" - if [[ "$BASE" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - NEXT="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$(( BASH_REMATCH[3] + 1 ))" - else - echo "::error::Could not resolve public SDK latest version (got '$PUBLIC_LATEST'); refusing to publish a canary with an unknown base." - exit 1 - fi - SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}" - if [[ ! "$SDK_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then - echo "::error::Computed SDK canary version '$SDK_VERSION' is not valid semver." - exit 1 - fi - echo "SDK canary version: $SDK_VERSION" - echo "SDK_VERSION=$SDK_VERSION" >> "$GITHUB_OUTPUT" - - - name: Set package version and pin runtime dependency - env: - SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }} - run: | - set -euo pipefail - npm version "$SDK_VERSION" --no-git-tag-version --allow-same-version - # Exact pin (no caret) so the published SDK canary depends on precisely - # the runtime version that was just tested by the e2e gate. - npm pkg set "dependencies.@github/copilot=$RUNTIME_VERSION" - echo "Pinned @github/copilot to $(npm pkg get dependencies.@github/copilot)" - - - name: Build SDK - run: npm run build - - - name: Azure Login (OIDC -> id-cpd-ci) - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci - tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" - allow-no-subscriptions: true - - # Auth-only .npmrc: just the two token lines, NO scoped registry line. - # The publish target is supplied explicitly via publishConfig + --registry. - - name: Configure feed auth (.npmrc) - run: | - set -euo pipefail - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - # Derive the protocol-relative auth scopes from FEED_URL (single source - # of truth). NO scoped @github:registry line here — publish target is - # supplied explicitly via publishConfig + --registry. - FEED_AUTH_REGISTRY="${FEED_URL#https:}" - FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - printf '%s\n' \ - "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ - "${FEED_AUTH_BASE}:_authToken=${TOKEN}" > .npmrc - echo "Wrote auth-only .npmrc to ./nodejs" - - # Belt and suspenders (2 of 3): pin the publish target in the package too. - - name: Set publishConfig registry - run: npm pkg set "publishConfig.registry=$FEED_URL" - - # Belt and suspenders (3 of 3): fail loudly unless the effective publish - # target is the internal feed. Guards against ever reaching public npm. - - name: Assert publish target is the internal feed - run: | - set -euo pipefail - EFFECTIVE="$(npm pkg get publishConfig.registry | tr -d '"')" - echo "Effective publishConfig.registry: $EFFECTIVE" - if [ "$EFFECTIVE" != "$FEED_URL" ]; then - echo "::error::publishConfig.registry ('$EFFECTIVE') is not the internal feed ('$FEED_URL'). Refusing to publish." - exit 1 - fi - - - name: Publish SDK canary to internal feed - run: npm publish --registry "$FEED_URL" - - - name: Summarize published canary - env: - SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }} - run: | - set -euo pipefail - { - echo "## SDK canary published" - echo "" - echo "| | |" - echo "| --- | --- |" - echo "| Runtime consumed | \`@github/copilot@${RUNTIME_VERSION}\` |" - echo "| Canary SDK produced | \`@github/copilot-sdk@${SDK_VERSION}\` |" - echo "| Feed | ${FEED_URL} |" - } >> "$GITHUB_STEP_SUMMARY" +name: "SDK Canary Test/Publish" + +# Nightly-style canary pipeline. First installs an explicit version of the +# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite +# against it to prove runtime <-> SDK compatibility. When that gate passes (and +# mode allows), publishes an SDK canary pinned to the tested runtime to the +# internal Azure Artifacts feed only (never public npm). + +env: + HUSKY: 0 + # Internal org-scoped Azure Artifacts feed — single source of truth so the + # feed name isn't repeated across steps. The SDK canary publishes here and + # (when runtime_source=internal) installs the runtime from here; it must NEVER + # reach public npm (@github/copilot-sdk is a live public package). + FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/ + # Azure DevOps resource ID used to mint an ADO access token for the feed. + ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 + +on: + workflow_dispatch: + inputs: + runtime_version: + description: "Exact github/copilot-cli release (public) or @github/copilot package version (internal)" + required: true + type: string + runtime_source: + description: "Where to install the runtime from" + required: true + type: choice + options: + - public + - internal + default: public + mode: + description: "publish (tests must pass), publish-force (publish even if tests fail), or tests-only (run gate, never publish)" + required: false + type: choice + default: publish + options: + - publish + - publish-force + - tests-only + repository_dispatch: + types: [runtime-canary] + +permissions: + contents: read + id-token: write + +# Serialize runs per ref so two overlapping canary runs can't race the feed +# publish. cancel-in-progress: false — never kill an in-flight publish. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + resolve: + name: "Resolve runtime inputs" + if: github.event.repository.fork == false + runs-on: ubuntu-latest + permissions: {} + outputs: + RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} + RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }} + PUBLISH_MODE: ${{ steps.normalize.outputs.PUBLISH_MODE }} + steps: + # Normalize whichever trigger fired into a single (RUNTIME_VERSION, + # RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step + # references. workflow_dispatch reads the human-supplied inputs; + # repository_dispatch reads client_payload and forces source=internal + # (a runtime canary only exists on the feed), defaulting mode to publish. + - name: Normalize inputs + id: normalize + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ inputs.runtime_version }} + INPUT_SOURCE: ${{ inputs.runtime_source }} + INPUT_MODE: ${{ inputs.mode }} + PAYLOAD_VERSION: ${{ github.event.client_payload.runtime_version }} + PAYLOAD_SOURCE: ${{ github.event.client_payload.runtime_source }} + PAYLOAD_MODE: ${{ github.event.client_payload.mode }} + run: | + set -euo pipefail + case "$EVENT_NAME" in + workflow_dispatch) + VERSION="$INPUT_VERSION" + SOURCE="$INPUT_SOURCE" + MODE="$INPUT_MODE" + ;; + repository_dispatch) + VERSION="$PAYLOAD_VERSION" + # A runtime canary only ever exists on the internal feed. + SOURCE="${PAYLOAD_SOURCE:-internal}" + MODE="${PAYLOAD_MODE:-publish}" + ;; + *) + echo "::error::Unsupported event '$EVENT_NAME'." + exit 1 + ;; + esac + if [ -z "$VERSION" ]; then echo "::error::Could not determine runtime version."; exit 1; fi + if [ -z "$SOURCE" ]; then SOURCE="public"; fi + case "$SOURCE" in + public|internal) ;; + *) echo "::error::Invalid runtime source '$SOURCE'. Expected one of: public, internal."; exit 1 ;; + esac + if [ -z "$MODE" ]; then MODE="publish"; fi + case "$MODE" in + publish|publish-force|tests-only) ;; + *) echo "::error::Invalid publish mode '$MODE'. Expected one of: publish, publish-force, tests-only."; exit 1 ;; + esac + echo "Resolved RUNTIME_VERSION=$VERSION RUNTIME_SOURCE=$SOURCE PUBLISH_MODE=$MODE" + echo "RUNTIME_VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "RUNTIME_SOURCE=$SOURCE" >> "$GITHUB_OUTPUT" + echo "PUBLISH_MODE=$MODE" >> "$GITHUB_OUTPUT" + + - name: Validate runtime version (semver) + env: + RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} + run: | + if [[ ! "$RUNTIME_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "::error::Invalid runtime version '$RUNTIME_VERSION'. Expected semver (e.g. 1.0.69 or 1.0.70-canary.abc123)." + exit 1 + fi + + test: + name: "E2E tests (${{ matrix.os }})" + needs: resolve + if: github.event.repository.fork == false + environment: cicd + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + env: + POWERSHELL_UPDATECHECK: Off + RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} + RUNTIME_SOURCE: ${{ needs.resolve.outputs.RUNTIME_SOURCE }} + defaults: + run: + shell: bash + working-directory: ./nodejs + steps: + - uses: actions/checkout@v6.0.2 + + - uses: actions/setup-node@v6 + with: + cache: "npm" + cache-dependency-path: "./nodejs/package-lock.json" + node-version: 22 + + - name: Install SDK dependencies + run: npm ci --ignore-scripts + + - name: Install test harness dependencies + working-directory: ./test/harness + run: npm ci --ignore-scripts + + - name: Azure Login (OIDC -> id-cpd-ci) + if: env.RUNTIME_SOURCE == 'internal' + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci + tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" + allow-no-subscriptions: true + + # Route ONLY @github/* (the runtime + its platform packages) to the + # internal feed via a scoped registry. All other deps (e.g. detect-libc) + # still resolve from public npm. A global --registry would break because + # detect-libc is not on the feed. + - name: Configure canary feed (.npmrc) + if: env.RUNTIME_SOURCE == 'internal' + run: | + set -euo pipefail + TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" + echo "::add-mask::$TOKEN" + # Derive the protocol-relative auth scopes from FEED_URL so the feed + # name lives in exactly one place (the workflow-level env). + FEED_AUTH_REGISTRY="${FEED_URL#https:}" + FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" + NPMRC="$(printf '%s\n' \ + "@github:registry=${FEED_URL}" \ + "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ + "${FEED_AUTH_BASE}:_authToken=${TOKEN}")" + printf '%s\n' "$NPMRC" > .npmrc + echo "Wrote scoped @github registry .npmrc to ./nodejs" + + - name: Override runtime version + run: | + set -euo pipefail + if [ "$RUNTIME_SOURCE" = "internal" ]; then + echo "Installing internal @github/copilot@${RUNTIME_VERSION}" + npm install "@github/copilot@${RUNTIME_VERSION}" --save-exact --ignore-scripts + node scripts/set-cli-version.js "$RUNTIME_VERSION" --npm-package + else + echo "Pinning github/copilot-cli release ${RUNTIME_VERSION}" + node scripts/set-cli-version.js "$RUNTIME_VERSION" + npm install --ignore-scripts + fi + + - name: Verify release runtime + run: | + set -euo pipefail + runtime_path=$(npm run --silent prepare:runtime -- --print-path) + node -e " + const fs = require('node:fs'); + const path = require('node:path'); + const runtime = process.argv[1]; + const runtimeStat = fs.statSync(runtime); + if (!runtimeStat.isFile()) throw new Error('Runtime wrapper is not a file'); + if (process.platform !== 'win32' && (runtimeStat.mode & 0o111) === 0) { + throw new Error('Runtime wrapper is not executable'); + } + if (!fs.statSync(path.join(path.dirname(runtime), 'runtime.node')).isFile()) { + throw new Error('runtime.node is not adjacent to the runtime wrapper'); + } + " "$runtime_path" + legacy_path=$(npm run --silent prepare:runtime -- --print-legacy-path) + node "$legacy_path" --version | grep -F "$RUNTIME_VERSION" + echo "COPILOT_CLI_PATH=$runtime_path" >> "$GITHUB_ENV" + + - name: Build SDK + run: npm run build + + - name: Warm up PowerShell + if: runner.os == 'Windows' + run: pwsh.exe -Command "Write-Host 'PowerShell ready'" + + - name: Run Node.js SDK e2e tests + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: npm test + + publish: + name: "Publish SDK canary (internal feed)" + needs: [resolve, test] + # Publish runs only when the gate permits it. Mode governs behavior: + # - tests-only: never publish (skips this job entirely). + # - publish: publish only when the e2e gate is green (the default for both + # the human and automated triggers). + # - publish-force: publish even on a non-green gate — a human-acknowledged + # flake override, audited via the ::warning:: step below and the run actor. + # publish-force only skips the e2e *signal* — the publish job still runs the + # build (so a broken build can't publish) and enforces the feed-only guards. + if: > + !cancelled() && + github.event.repository.fork == false && + needs.resolve.result == 'success' && + needs.resolve.outputs.PUBLISH_MODE != 'tests-only' && + (needs.test.result == 'success' || + needs.resolve.outputs.PUBLISH_MODE == 'publish-force') + environment: cicd + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + env: + RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} + RUNTIME_SOURCE: ${{ needs.resolve.outputs.RUNTIME_SOURCE }} + defaults: + run: + shell: bash + working-directory: ./nodejs + steps: + - name: Warn — publishing despite failed e2e gate (publish-force) + # always() so this audit is never skipped by prior-step status; it fires + # specifically when publish proceeded on a non-green gate via publish-force. + # Runs at the workspace root because it executes before checkout, so the + # job's default working-directory (./nodejs) does not exist yet. + if: always() && needs.test.result != 'success' && needs.resolve.outputs.PUBLISH_MODE == 'publish-force' + working-directory: ${{ github.workspace }} + run: | + echo "::warning title=e2e gate bypassed::Publishing SDK canary despite a non-passing e2e gate (test job result: ${{ needs.test.result }}) via publish-force. Triggered by '${{ github.actor }}' through '${{ github.event_name }}'. The e2e signal was bypassed; build + feed-only guards still apply." + + - uses: actions/checkout@v6.0.2 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + + # Default public registry: installs build deps and the currently pinned + # runtime. Do NOT write any feed .npmrc or scoped @github:registry line + # here, or npm ci would try to fetch the runtime from the upstream-less + # feed and 404. + - name: Install SDK dependencies + run: npm ci --ignore-scripts + + - name: Compute SDK canary version + id: sdkver + env: + RUN_NUMBER: ${{ github.run_number }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + SHORT_SHA="${SHA:0:7}" + # Base the canary on the NEXT patch of the public SDK latest so canaries + # correlate with public releases: they sort ABOVE the current public + # latest and BELOW the eventual real release of that next patch (a + # prerelease of X.Y.Z always sorts below X.Y.Z), so a canary can never + # shadow the real release when it ships. + # Reuse the repo's own version helper (scripts/get-version.js) so this + # stays consistent with publish.yml: `current` returns the latest public + # dist-tag version, read-only from public npm (never the feed), then + # we bump the patch ourselves to keep strict patch+1 semantics. + PUBLIC_LATEST="$(node scripts/get-version.js current || true)" + BASE="${PUBLIC_LATEST%%-*}"; BASE="${BASE%%+*}" + if [[ "$BASE" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + NEXT="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$(( BASH_REMATCH[3] + 1 ))" + else + echo "::error::Could not resolve public SDK latest version (got '$PUBLIC_LATEST'); refusing to publish a canary with an unknown base." + exit 1 + fi + SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}" + if [[ ! "$SDK_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "::error::Computed SDK canary version '$SDK_VERSION' is not valid semver." + exit 1 + fi + echo "SDK canary version: $SDK_VERSION" + echo "SDK_VERSION=$SDK_VERSION" >> "$GITHUB_OUTPUT" + + - name: Set package and runtime versions + env: + SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }} + run: | + set -euo pipefail + npm version "$SDK_VERSION" --no-git-tag-version --allow-same-version + if [ "$RUNTIME_SOURCE" = "internal" ]; then + npm pkg set "dependencies.@github/copilot=$RUNTIME_VERSION" + node scripts/set-cli-version.js "$RUNTIME_VERSION" --npm-package + else + node scripts/set-cli-version.js "$RUNTIME_VERSION" + fi + echo "Pinned github/copilot-cli release to $(npm pkg get copilotCliVersion)" + + - name: Build SDK + run: npm run build + + - name: Package public release runtimes + if: env.RUNTIME_SOURCE == 'public' + run: npm run pack:release + + - name: Azure Login (OIDC -> id-cpd-ci) + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci + tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" + allow-no-subscriptions: true + + # Auth-only .npmrc: just the two token lines, NO scoped registry line. + # The publish target is supplied explicitly via publishConfig + --registry. + - name: Configure feed auth (.npmrc) + run: | + set -euo pipefail + TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" + echo "::add-mask::$TOKEN" + # Derive the protocol-relative auth scopes from FEED_URL (single source + # of truth). NO scoped @github:registry line here — publish target is + # supplied explicitly via publishConfig + --registry. + FEED_AUTH_REGISTRY="${FEED_URL#https:}" + FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" + printf '%s\n' \ + "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ + "${FEED_AUTH_BASE}:_authToken=${TOKEN}" > .npmrc + echo "Wrote auth-only .npmrc to ./nodejs" + + # Belt and suspenders (2 of 3): pin the publish target in the package too. + - name: Set publishConfig registry + run: npm pkg set "publishConfig.registry=$FEED_URL" + + # Belt and suspenders (3 of 3): fail loudly unless the effective publish + # target is the internal feed. Guards against ever reaching public npm. + - name: Assert publish target is the internal feed + run: | + set -euo pipefail + EFFECTIVE="$(npm pkg get publishConfig.registry | tr -d '"')" + echo "Effective publishConfig.registry: $EFFECTIVE" + if [ "$EFFECTIVE" != "$FEED_URL" ]; then + echo "::error::publishConfig.registry ('$EFFECTIVE') is not the internal feed ('$FEED_URL'). Refusing to publish." + exit 1 + fi + + - name: Publish SDK canary to internal feed + run: | + set -euo pipefail + if [ "$RUNTIME_SOURCE" = "internal" ]; then + node scripts/npm-release.js publish . canary "$FEED_URL" azure + exit + fi + shopt -s nullglob + TARBALLS=(./github-copilot-sdk-*.tgz) + if [ "${#TARBALLS[@]}" -ne 9 ]; then + echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." + exit 1 + fi + MAIN_TARBALL="" + for TARBALL in "${TARBALLS[@]}"; do + PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" + if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then + MAIN_TARBALL="$TARBALL" + else + node scripts/npm-release.js publish "$TARBALL" canary "$FEED_URL" azure + fi + done + if [ -z "$MAIN_TARBALL" ]; then + echo "::error::Main @github/copilot-sdk tarball not found." + exit 1 + fi + node scripts/npm-release.js publish "$MAIN_TARBALL" canary "$FEED_URL" azure + + - name: Summarize published canary + env: + SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }} + run: | + set -euo pipefail + { + echo "## SDK canary published" + echo "" + echo "| | |" + echo "| --- | --- |" + if [ "$RUNTIME_SOURCE" = "public" ]; then + echo "| Runtime consumed | \`github/copilot-cli@${RUNTIME_VERSION}\` release assets |" + else + echo "| Runtime consumed | \`@github/copilot@${RUNTIME_VERSION}\` |" + fi + echo "| Canary SDK produced | \`@github/copilot-sdk@${SDK_VERSION}\` |" + echo "| Feed | ${FEED_URL} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/update-copilot-dependency.yml b/.github/workflows/update-copilot-dependency.yml index 9646366ad5..2870ad27f7 100644 --- a/.github/workflows/update-copilot-dependency.yml +++ b/.github/workflows/update-copilot-dependency.yml @@ -1,10 +1,10 @@ -name: "Update @github/copilot Dependency" +name: "Update Copilot CLI Version" on: workflow_dispatch: inputs: version: - description: "Target version of @github/copilot (e.g. 0.0.420)" + description: "Target github/copilot-cli release version (e.g. 1.0.83-0)" required: true type: string @@ -14,7 +14,7 @@ permissions: jobs: update: - name: "Update @github/copilot to ${{ inputs.version }}" + name: "Update Copilot CLI to ${{ inputs.version }}" runs-on: ubuntu-latest steps: - name: Validate version input @@ -56,17 +56,13 @@ jobs: toolchain: nightly-2026-04-14 components: rustfmt - - name: Update @github/copilot in nodejs + - name: Update the Node.js CLI release pin env: VERSION: ${{ inputs.version }} working-directory: ./nodejs - run: npm install "@github/copilot@$VERSION" - - - name: Update @github/copilot in test harness - env: - VERSION: ${{ inputs.version }} - working-directory: ./test/harness - run: npm install "@github/copilot@$VERSION" + run: | + node scripts/set-cli-version.js "$VERSION" + npm install --ignore-scripts - name: Refresh nodejs/samples lockfile working-directory: ./nodejs/samples @@ -165,19 +161,20 @@ jobs: exit 0 fi - git commit -m "Update @github/copilot to $VERSION + git commit -m "Update Copilot CLI to $VERSION - - Updated nodejs and test harness dependencies + - Updated the Node.js CLI release pin - Re-ran code generators - Formatted generated code" git push origin "$BRANCH" --force-with-lease PR_BODY=$(cat <<'BODY_EOF' - Automated update of `@github/copilot` to version `PLACEHOLDER_VERSION`. + Automated update of the Copilot CLI release to version `PLACEHOLDER_VERSION`. ### Changes - - Updated `@github/copilot` in `nodejs/package.json` and `test/harness/package.json` + - Updated the release pin in `nodejs/package.json` + - Validated the release assets listed in `SHA256SUMS.txt` - Re-ran all code generators (`scripts/codegen`) - Formatted generated output - Updated Java codegen dependency, POM property, and regenerated Java types @@ -208,7 +205,7 @@ jobs: ### Next steps When ready, click **Ready for review** to trigger CI checks. - > Created by the **Update @github/copilot Dependency** workflow. + > Created by the **Update Copilot CLI Version** workflow. BODY_EOF ) PR_BODY="${PR_BODY//PLACEHOLDER_VERSION/$VERSION}" diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 92650ee60c..9bd7a5fb54 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -2544,17 +2544,23 @@ private static string ResolveRuntimePathForExplicitCli(string cliPath) var fullEntrypoint = Path.GetFullPath(cliPath); var directory = Path.GetDirectoryName(fullEntrypoint) ?? throw new InvalidOperationException($"Could not determine directory for '{cliPath}'."); - var flatLibraryPath = Path.Combine(directory, FfiRuntimeHost.GetRuntimeLibraryFileName()); + var flatLibraryPath = Path.GetFullPath( + $"{directory}{Path.DirectorySeparatorChar}{FfiRuntimeHost.GetRuntimeLibraryFileName()}"); if (File.Exists(flatLibraryPath)) { return flatLibraryPath; } + var adjacentPrebuildPath = Path.Combine(directory, "runtime.node"); + if (File.Exists(adjacentPrebuildPath)) + { + return adjacentPrebuildPath; + } var prebuildsLibraryPath = Path.Combine( directory, "prebuilds", GetNapiPrebuildsFolderOrThrow(), "runtime.node"); return File.Exists(prebuildsLibraryPath) ? prebuildsLibraryPath : throw new InvalidOperationException( - $"FFI runtime library not found. Looked for '{flatLibraryPath}' and '{prebuildsLibraryPath}'."); + $"FFI runtime library not found. Looked for '{flatLibraryPath}', '{adjacentPrebuildPath}', and '{prebuildsLibraryPath}'."); } /// diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index f48fb802d7..e5a2d9fb98 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -63,10 +63,10 @@ - + - + diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index 95770dba8e..aca299dd27 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -153,7 +153,7 @@ <_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*" - Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot-sdk\**\*;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\preloads\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sdk\**\*;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" /> + Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" /> <_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*" Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*" /> @@ -193,7 +193,7 @@ Condition="'$(CopilotCliBinaryPath)' != ''" /> <_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*" - Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot-sdk\**\*;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\preloads\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sdk\**\*;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" + Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')" /> <_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*" Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*" diff --git a/dotnet/test/E2E/EventFidelityE2ETests.cs b/dotnet/test/E2E/EventFidelityE2ETests.cs index 8882f972d5..da9869757f 100644 --- a/dotnet/test/E2E/EventFidelityE2ETests.cs +++ b/dotnet/test/E2E/EventFidelityE2ETests.cs @@ -12,7 +12,7 @@ namespace GitHub.Copilot.Test.E2E; /// Verifies the shape and ordering of s emitted from the /// runtime: every event has an id and timestamp, user/assistant messages carry /// content, tool execution events carry a toolCallId, and -/// session.idle is the last event of a turn. Mirrors +/// session.idle follows the final assistant message of a turn. Mirrors /// nodejs/test/e2e/event_fidelity.e2e.test.ts. /// public class EventFidelityE2ETests(E2ETestFixture fixture, ITestOutputHelper output) @@ -43,9 +43,12 @@ await session.SendAndWaitAsync(new MessageOptions var assistantIdx = types.LastIndexOf("assistant.message"); Assert.True(userIdx < assistantIdx, $"Expected user.message ({userIdx}) before last assistant.message ({assistantIdx})"); - // session.idle should be the last event we observed + // session.idle should complete the conversational turn. Post-turn + // metadata events may arrive after it on slower target frameworks. var idleIdx = types.LastIndexOf("session.idle"); - Assert.Equal(types.Count - 1, idleIdx); + Assert.True( + assistantIdx < idleIdx, + $"Expected last assistant.message ({assistantIdx}) before session.idle ({idleIdx}): {string.Join(", ", types)}"); await session.DisposeAsync(); } diff --git a/dotnet/test/E2E/MultiClientE2ETests.cs b/dotnet/test/E2E/MultiClientE2ETests.cs index 4dbe7190ad..7597aa580b 100644 --- a/dotnet/test/E2E/MultiClientE2ETests.cs +++ b/dotnet/test/E2E/MultiClientE2ETests.cs @@ -324,6 +324,9 @@ public async Task Disconnecting_Client_Removes_Its_Tools() // Disconnect client 2 await Client2.ForceStopAsync(); + // Give the server time to process the connection close and remove tools. + await Task.Delay(500); + // Recreate client2 for cleanup var port = Client1.RuntimePort!.Value; _client2 = Ctx.CreateClient(options: new CopilotClientOptions diff --git a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs index bf1ed687c1..e48dbbbcbc 100644 --- a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs +++ b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs @@ -140,10 +140,12 @@ await session1.SendAsync(new MessageOptions } [Description("Looks up a value after resumption")] - async Task BlockingExternalTool([Description("Value to look up")] string value) + async Task BlockingExternalTool( + [Description("Value to look up")] string value, + CancellationToken cancellationToken) { originalToolStarted.TrySetResult(value); - return await releaseOriginalTool.Task; + return await releaseOriginalTool.Task.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); } } @@ -274,11 +276,13 @@ await TestHelper.WaitForConditionAsync( } [Description("Looks up a value after resumption")] - async Task BlockingExternalTool([Description("Value to look up")] string value) + async Task BlockingExternalTool( + [Description("Value to look up")] string value, + CancellationToken cancellationToken) { Interlocked.Increment(ref invocationCount); originalToolStarted.TrySetResult(value); - return await releaseOriginalTool.Task; + return await releaseOriginalTool.Task.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); } [Description("Looks up a value after resumption")] @@ -356,17 +360,21 @@ await Task.WhenAll( } [Description("Looks up the first value after resumption")] - async Task BlockingToolA([Description("Value to look up")] string value) + async Task BlockingToolA( + [Description("Value to look up")] string value, + CancellationToken cancellationToken) { originalToolAStarted.TrySetResult(value); - return await releaseOriginalToolA.Task; + return await releaseOriginalToolA.Task.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); } [Description("Looks up the second value after resumption")] - async Task BlockingToolB([Description("Value to look up")] string value) + async Task BlockingToolB( + [Description("Value to look up")] string value, + CancellationToken cancellationToken) { originalToolBStarted.TrySetResult(value); - return await releaseOriginalToolB.Task; + return await releaseOriginalToolB.Task.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); } } diff --git a/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs b/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs index 0a3513e4b5..3d017d8165 100644 --- a/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs +++ b/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs @@ -57,10 +57,22 @@ private CopilotClient CreateExtensionsClient() { return Ctx.CreateClient(options: new CopilotClientOptions { - Connection = RuntimeConnection.ForStdio(args: ["--yolo"]), + Connection = RuntimeConnection.ForStdio( + path: Ctx.GetLegacyCliPath(), + args: ["--yolo"]), }, environment: ExtensionsEnabledEnvironment()); } + private static SessionConfig CreateExtensionsSessionConfig(string? workingDirectory = null) + { + return new SessionConfig + { + EnableConfigDiscovery = true, + WorkingDirectory = workingDirectory, + OnPermissionRequest = PermissionHandler.ApproveAll, + }; + } + /// /// Writes a minimal user extension into {HomeDir}/extensions/{name}/extension.mjs. /// The body imports @github/copilot-sdk/extension, calls joinSession @@ -189,12 +201,9 @@ public async Task Discovers_Loads_And_Reports_Running_Extension(string sourceVal await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - WorkingDirectory = workingDirectory, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig(workingDirectory)); var ext = await WaitForExtensionAsync(session, extId, ExtensionStatus.Running); @@ -214,11 +223,9 @@ public async Task Disable_Then_Enable_Cycles_Extension_Status() await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); // Wait until the initial running state is observed before mutating. await WaitForExtensionAsync(session, extId, ExtensionStatus.Running); @@ -240,11 +247,9 @@ public async Task Reload_Picks_Up_Extension_Added_After_Session_Create() // Start the session BEFORE writing the extension so the initial discovery sees nothing. await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); // setupExtensionsForSession runs asynchronously; until it completes the // controller isn't installed and ReloadAsync throws "Extensions not @@ -285,11 +290,9 @@ public async Task Failed_Extension_Reports_Failed_Status() await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); var ext = await WaitForExtensionAsync(session, extId, ExtensionStatus.Failed); Assert.Equal(extId, ext.Id); @@ -306,11 +309,9 @@ public async Task Multiple_Extensions_Are_Discovered_Independently() await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); await WaitForExtensionAsync(session, ext1Id, ExtensionStatus.Running); await WaitForExtensionAsync(session, ext2Id, ExtensionStatus.Running); @@ -328,11 +329,9 @@ public async Task Reload_Preserves_Disabled_State_Across_Calls() await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); await WaitForExtensionAsync(session, extId, ExtensionStatus.Running); diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs index 0080cbc609..2554c6a4d1 100644 --- a/dotnet/test/Harness/E2ETestContext.cs +++ b/dotnet/test/Harness/E2ETestContext.cs @@ -3,9 +3,9 @@ *--------------------------------------------------------------------------------------------*/ using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text.RegularExpressions; namespace GitHub.Copilot.Test.Harness; @@ -14,6 +14,7 @@ public sealed class E2ETestContext : IAsyncDisposable { private const string DefaultGitHubToken = "fake-token-for-e2e-tests"; private static readonly TimeSpan s_gracefulClientStopTimeout = TimeSpan.FromSeconds(30); + private static readonly ConcurrentDictionary> s_preparedCliPaths = new(StringComparer.Ordinal); public string HomeDir { get; } public string WorkDir { get; } @@ -25,6 +26,8 @@ public sealed class E2ETestContext : IAsyncDisposable private readonly ReplayProxy _proxy; private readonly string _repoRoot; + private readonly Lazy _cliPath; + private readonly Lazy _legacyCliPath; private readonly object _clientsLock = new(); private readonly List _persistentClients = []; private readonly List _transientClients = []; @@ -36,6 +39,8 @@ private E2ETestContext(string homeDir, string workDir, string proxyUrl, ReplayPr ProxyUrl = proxyUrl; _proxy = proxy; _repoRoot = repoRoot; + _cliPath = GetCachedCliPath(repoRoot, "--print-path"); + _legacyCliPath = GetCachedCliPath(repoRoot, "--print-legacy-path"); } public static async Task CreateAsync() @@ -144,46 +149,56 @@ private static string FindRepoRoot() throw new InvalidOperationException("Could not find repository root"); } - private static string GetCliPath(string repoRoot) + private string GetCliPath() + => _cliPath.Value; + + public string GetLegacyCliPath() + => _legacyCliPath.Value; + + private static Lazy GetCachedCliPath(string repoRoot, string option) + { + var envPath = option == "--print-path" + ? Environment.GetEnvironmentVariable("COPILOT_CLI_PATH") + : null; + var cacheKey = $"{repoRoot}\0{option}\0{envPath}"; + return s_preparedCliPaths.GetOrAdd( + cacheKey, + _ => new Lazy( + () => PrepareCliPath(repoRoot, option), + LazyThreadSafetyMode.ExecutionAndPublication)); + } + + private static string PrepareCliPath(string repoRoot, string option) { var envPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); - if (!string.IsNullOrEmpty(envPath)) return envPath; - - // 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. - var githubModules = Path.Join(repoRoot, "nodejs", "node_modules", "@github"); - var packagePrefix = GetCliPackagePrefix(); - var candidates = Directory.Exists(githubModules) - ? Directory.EnumerateDirectories(githubModules, $"{packagePrefix}-*", SearchOption.TopDirectoryOnly) - .Select(directory => Path.Join(directory, "index.js")) - .Where(File.Exists) - .ToArray() - : []; - - return candidates.Length switch + if (option == "--print-path" && !string.IsNullOrEmpty(envPath)) return envPath; + + var startInfo = new ProcessStartInfo { - 1 => candidates[0], - 0 => throw new InvalidOperationException( - $"CLI package matching '{packagePrefix}-*' not found under {githubModules}. " + - "Run 'npm install' in the nodejs directory first."), - _ => throw new InvalidOperationException( - $"Multiple CLI packages matching '{packagePrefix}-*' found under {githubModules}: " + - string.Join(", ", candidates.Select(Path.GetDirectoryName))), + FileName = "node", + WorkingDirectory = Path.Join(repoRoot, "nodejs"), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + Arguments = $"node_modules/tsx/dist/cli.mjs scripts/prepare-runtime.ts {option}", }; - } - private static string GetCliPackagePrefix() - { - var platform = OperatingSystem.IsWindows() - ? "win32" - : OperatingSystem.IsMacOS() - ? "darwin" - : OperatingSystem.IsLinux() - ? RuntimeInformation.RuntimeIdentifier.StartsWith("linux-musl-", StringComparison.Ordinal) - ? "linuxmusl" - : "linux" - : throw new PlatformNotSupportedException("Unsupported operating system for Copilot CLI E2E tests."); - return $"copilot-{platform}"; + using var process = Process.Start(startInfo) + ?? throw new InvalidOperationException("Failed to start Node.js runtime preparation."); + var stdout = process.StandardOutput.ReadToEndAsync(); + var stderr = process.StandardError.ReadToEndAsync(); + process.WaitForExit(); + var output = stdout.GetAwaiter().GetResult(); + var lines = output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var cliPath = lines.Length == 0 ? string.Empty : lines[^1].Trim(); + var error = stderr.GetAwaiter().GetResult().Trim(); + if (process.ExitCode != 0 || string.IsNullOrEmpty(cliPath)) + throw new InvalidOperationException( + $"Failed to prepare the pinned Copilot CLI: {error}"); + if (!File.Exists(cliPath)) + throw new InvalidOperationException( + $"Pinned Copilot CLI was not created at {cliPath}."); + return cliPath; } public async Task ConfigureForTestAsync(string testFile, [CallerMemberName] string? testName = null) @@ -310,21 +325,20 @@ public CopilotClient CreateClient( // CopilotClient honors COPILOT_SDK_DEFAULT_CONNECTION (stdio by default, // or in-process); the CI matrix uses this to run the suite under both. // Tests that need a specific transport set options.Connection directly. - var cliPath = GetCliPath(_repoRoot); switch (options.Connection) { case null when !IsInProcess(null): // No explicit connection and not the in-process default: the // default resolves to stdio, so materialize it here so the // environment can be attached to the connection below. - options.Connection = RuntimeConnection.ForStdio(path: cliPath); + options.Connection = RuntimeConnection.ForStdio(path: GetCliPath()); break; case null: // In-process default: leave Connection unset so CopilotClient's // ResolveDefaultConnection honors COPILOT_SDK_DEFAULT_CONNECTION. break; case ChildProcessRuntimeConnection child when child.Path is null: - child.Path = cliPath; + child.Path = GetCliPath(); break; } diff --git a/dotnet/test/Unit/MSBuildTargetsTests.cs b/dotnet/test/Unit/MSBuildTargetsTests.cs index cab91e568f..0dd3073956 100644 --- a/dotnet/test/Unit/MSBuildTargetsTests.cs +++ b/dotnet/test/Unit/MSBuildTargetsTests.cs @@ -116,6 +116,9 @@ public async Task RuntimePackageAssets_AreFilteredAndCopiedToOutput() OperatingSystem.IsWindows() ? "copilot-runtime.exe" : "copilot-runtime", "wrapper"); sandbox.WriteRuntimeCacheAsset("ripgrep", "bin", GetNpmPlatform(), "rg", "ripgrep"); sandbox.WriteRuntimeCacheAsset("definitions", "future.json", "{}"); + sandbox.WriteRuntimeCacheAsset("copilot-sdk", "extension.js", "extension"); + sandbox.WriteRuntimeCacheAsset("preloads", "extension_bootstrap.mjs", "preload"); + sandbox.WriteRuntimeCacheAsset("sdk", "factory.js", "factory"); sandbox.WriteRuntimeCacheAsset("app.js", "excluded"); sandbox.WriteRuntimeCacheAsset("LICENSE.md", "excluded"); sandbox.WriteRuntimeCacheAsset("README.md", "excluded"); @@ -129,6 +132,9 @@ public async Task RuntimePackageAssets_AreFilteredAndCopiedToOutput() Assert.True(result.Succeeded, result.FailureMessage()); Assert.Equal("ripgrep", File.ReadAllText(sandbox.ExpectedRuntimeAsset("ripgrep", "bin", GetNpmPlatform(), "rg"))); Assert.Equal("{}", File.ReadAllText(sandbox.ExpectedRuntimeAsset("definitions", "future.json"))); + Assert.Equal("extension", File.ReadAllText(sandbox.ExpectedRuntimeAsset("copilot-sdk", "extension.js"))); + Assert.Equal("preload", File.ReadAllText(sandbox.ExpectedRuntimeAsset("preloads", "extension_bootstrap.mjs"))); + Assert.Equal("factory", File.ReadAllText(sandbox.ExpectedRuntimeAsset("sdk", "factory.js"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("app.js"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("LICENSE.md"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("README.md"))); diff --git a/go/README.md b/go/README.md index 6c85f93ae1..801ad8556c 100644 --- a/go/README.md +++ b/go/README.md @@ -20,7 +20,10 @@ go get github.com/github/copilot-sdk/go Try the interactive chat sample (from the repo root): ```bash -cd go/samples +cd nodejs +npm ci +export COPILOT_CLI_PATH="$(npm run --silent prepare:runtime -- --print-path)" +cd ../go/samples go run chat.go ``` diff --git a/go/client.go b/go/client.go index 735e8ae414..5789fca5eb 100644 --- a/go/client.go +++ b/go/client.go @@ -2445,11 +2445,9 @@ func (c *Client) setupNotificationHandler() { } if c.options.RequestHandler != nil { + llmInference := c.RPC.LlmInference handlers.LlmInference = newCopilotRequestAdapter(c.options.RequestHandler, func() *rpc.ServerLlmInferenceAPI { - if c.RPC == nil { - return nil - } - return c.RPC.LlmInference + return llmInference }) } if c.options.OnGitHubTelemetry != nil { diff --git a/go/client_test.go b/go/client_test.go index e74c234637..dffc15d74e 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1326,15 +1326,36 @@ func TestClient_SessionIdleTimeoutSeconds(t *testing.T) { }) } -func findCLIPathForTest() string { - base, err := filepath.Abs("../nodejs/node_modules/@github") - if err == nil { - matches, _ := filepath.Glob(filepath.Join(base, "copilot-*", "index.js")) - if len(matches) > 0 { - return matches[0] +func findCLIPathForTest(t *testing.T) string { + t.Helper() + + if cliPath := os.Getenv("COPILOT_CLI_PATH"); cliPath != "" { + if info, err := os.Stat(cliPath); err == nil && !info.IsDir() { + return cliPath } + t.Fatalf("COPILOT_CLI_PATH does not point to a file: %s", cliPath) + } + + nodeDir, err := filepath.Abs("../nodejs") + if err != nil { + t.Fatal(err) } - return "" + command := exec.Command( + "node", + "node_modules/tsx/dist/cli.mjs", + "scripts/prepare-runtime.ts", + "--print-path", + ) + command.Dir = nodeDir + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("failed to prepare pinned Copilot CLI: %v\n%s", err, output) + } + cliPath := strings.TrimSpace(string(output)) + if info, err := os.Stat(cliPath); err != nil || info.IsDir() { + t.Fatalf("prepared Copilot CLI path is not a file: %q", cliPath) + } + return cliPath } func TestCreateSessionRequest_ClientName(t *testing.T) { @@ -2181,10 +2202,7 @@ func TestListModelsHandlerCachesResults(t *testing.T) { } func TestClient_StartContextCancellationDoesNotKillProcess(t *testing.T) { - cliPath := findCLIPathForTest() - if cliPath == "" { - t.Skip("CLI not found") - } + cliPath := findCLIPathForTest(t) client := NewClient(&ClientOptions{Connection: StdioConnection{Path: cliPath}}) t.Cleanup(func() { client.ForceStop() }) @@ -2207,10 +2225,7 @@ func TestClient_StartContextCancellationDoesNotKillProcess(t *testing.T) { } func TestClient_StartStopRace(t *testing.T) { - cliPath := findCLIPathForTest() - if cliPath == "" { - t.Skip("CLI not found") - } + cliPath := findCLIPathForTest(t) client := NewClient(&ClientOptions{Connection: StdioConnection{Path: cliPath}}) defer client.ForceStop() errChan := make(chan error) diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index 89f99daf1b..7ee078eea0 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -8,7 +8,7 @@ // --platform: Target platform using Go conventions (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64). Defaults to current platform. // --output: Output directory for embedded artifacts. Defaults to the current directory. // --cli-version: CLI version to download. If not specified, automatically detects from the copilot-sdk version in go.mod. -// --check-only: Check that embedded CLI version matches the detected version from package-lock.json without downloading. Exits with error if versions don't match. +// --check-only: Check that embedded CLI version matches the detected version from package.json without downloading. Exits with error if versions don't match. package main import ( @@ -37,6 +37,7 @@ import ( const ( // Keep these URLs centralized so reviewers can verify all outbound calls in one place. sdkModule = "github.com/github/copilot-sdk/go" + packageJSONURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package.json" packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" tarballURLFmt = "https://registry.npmjs.org/@github/copilot-%s/-/copilot-%s-%s.tgz" licenseTarballFmt = "https://registry.npmjs.org/@github/copilot/-/copilot-%s.tgz" @@ -262,8 +263,8 @@ func detectPackageName(dir, goos, goarch string) (string, error) { // detectCLIVersion detects the CLI version by: // 1. Running "go list -m" to get the copilot-sdk version from the user's go.mod -// 2. Fetching the package-lock.json from the SDK repo at that version -// 3. Extracting the @github/copilot CLI version from it +// 2. Fetching package.json from the SDK repo at that version +// 3. Extracting the pinned Copilot CLI version from it func detectCLIVersion() (string, error) { // Get the SDK version from the user's go.mod sdkVersion, err := getSDKVersion() @@ -273,7 +274,7 @@ func detectCLIVersion() (string, error) { fmt.Printf("Found copilot-sdk %s in go.mod\n", sdkVersion) - // Fetch package-lock.json from the SDK repo at that version + // Fetch package.json from the SDK repo at that version cliVersion, err := fetchCLIVersionFromRepo(sdkVersion) if err != nil { return "", fmt.Errorf("failed to fetch CLI version: %w", err) @@ -301,7 +302,7 @@ func getSDKVersion() (string, error) { return version, nil } -// fetchCLIVersionFromRepo fetches package-lock.json from GitHub and extracts the CLI version. +// fetchCLIVersionFromRepo fetches package.json from GitHub and extracts the CLI version. func fetchCLIVersionFromRepo(sdkVersion string) (string, error) { // Convert Go module version to Git ref // v0.1.0 -> v0.1.0 @@ -319,7 +320,7 @@ func fetchCLIVersionFromRepo(sdkVersion string) (string, error) { } } - url := fmt.Sprintf(packageLockURLFmt, gitRef) + url := fmt.Sprintf(packageJSONURLFmt, gitRef) fmt.Printf("Fetching %s...\n", url) resp, err := http.Get(url) @@ -329,7 +330,35 @@ func fetchCLIVersionFromRepo(sdkVersion string) (string, error) { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to fetch package-lock.json: %s", resp.Status) + return "", fmt.Errorf("failed to fetch package.json: %s", resp.Status) + } + + var packageJSON struct { + CopilotCLIVersion string `json:"copilotCliVersion"` + } + + if err := json.NewDecoder(resp.Body).Decode(&packageJSON); err != nil { + return "", fmt.Errorf("failed to parse package.json: %w", err) + } + + if packageJSON.CopilotCLIVersion == "" { + return fetchLegacyCLIVersionFromRepo(gitRef) + } + + return packageJSON.CopilotCLIVersion, nil +} + +func fetchLegacyCLIVersionFromRepo(gitRef string) (string, error) { + url := fmt.Sprintf(packageLockURLFmt, gitRef) + fmt.Printf("Falling back to %s...\n", url) + + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("failed to fetch legacy package-lock.json: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch legacy package-lock.json: %s", resp.Status) } var packageLock struct { @@ -337,16 +366,13 @@ func fetchCLIVersionFromRepo(sdkVersion string) (string, error) { Version string `json:"version"` } `json:"packages"` } - if err := json.NewDecoder(resp.Body).Decode(&packageLock); err != nil { - return "", fmt.Errorf("failed to parse package-lock.json: %w", err) + return "", fmt.Errorf("failed to parse legacy package-lock.json: %w", err) } - pkg, ok := packageLock.Packages["node_modules/@github/copilot"] if !ok || pkg.Version == "" { - return "", fmt.Errorf("could not find @github/copilot version in package-lock.json") + return "", fmt.Errorf("could not find copilotCliVersion in package.json or @github/copilot in package-lock.json") } - return pkg.Version, nil } @@ -509,9 +535,9 @@ func runtimeWrapperName(binaryName string) string { var hostlessExcludedTopLevel = map[string]bool{ "app.js": true, "assets": true, "changelog.json": true, "copilot": true, "copilot.exe": true, - "copilot-sdk": true, "foundry-local-sdk": true, "index.js": true, "napi-oop-runtime": true, - "LICENSE.md": true, "npm-loader.js": true, "package.json": true, "preloads": true, "pvrecorder": true, - "queries": true, "README.md": true, "sdk": true, "sea-loader.js": true, "webview": true, + "foundry-local-sdk": true, "index.js": true, "napi-oop-runtime": true, "LICENSE.md": true, + "npm-loader.js": true, "package.json": true, "pvrecorder": true, "queries": true, "README.md": true, + "sea-loader.js": true, "webview": true, } func hostlessRuntimePath(name, npmPlatform, wrapperName string) (string, bool) { diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go index f41afe43f5..9c09559b66 100644 --- a/go/cmd/bundler/main_test.go +++ b/go/cmd/bundler/main_test.go @@ -22,6 +22,9 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t "package/prebuilds/linux-x64/copilot-runtime": "wrapper", "package/ripgrep/bin/linux-x64/rg": "ripgrep", "package/definitions/future.json": "{}", + "package/copilot-sdk/extension.js": "extension", + "package/preloads/extension_bootstrap.mjs": "preload", + "package/sdk/factory.js": "factory", "package/app.js": "excluded", "package/LICENSE.md": "excluded", "package/README.md": "excluded", @@ -35,7 +38,11 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t } files := readTarGz(t, output) - if files["ripgrep/bin/linux-x64/rg"] != "ripgrep" || files["definitions/future.json"] != "{}" { + if files["ripgrep/bin/linux-x64/rg"] != "ripgrep" || + files["definitions/future.json"] != "{}" || + files["copilot-sdk/extension.js"] != "extension" || + files["preloads/extension_bootstrap.mjs"] != "preload" || + files["sdk/factory.js"] != "factory" { t.Fatalf("retained assets = %#v", files) } for _, excluded := range []string{ diff --git a/go/internal/e2e/event_fidelity_e2e_test.go b/go/internal/e2e/event_fidelity_e2e_test.go index e7cc4bfb37..8606da22d9 100644 --- a/go/internal/e2e/event_fidelity_e2e_test.go +++ b/go/internal/e2e/event_fidelity_e2e_test.go @@ -333,8 +333,8 @@ func TestEventFidelityE2E(t *testing.T) { } idleIdx := lastEventFidelityTypeIndex(types, copilot.SessionEventTypeSessionIdle) - if idleIdx != len(types)-1 { - t.Fatalf("Expected session.idle to be last event; idleIdx=%d len=%d types=%v", idleIdx, len(types), types) + if idleIdx < 0 || assistantIdx >= idleIdx { + t.Fatalf("Expected last assistant.message before session.idle; assistantIdx=%d idleIdx=%d types=%v", assistantIdx, idleIdx, types) } }) diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index 037265f9de..0374cc5130 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -2,6 +2,7 @@ package testharness import ( "os" + "os/exec" "path/filepath" "regexp" "runtime" @@ -29,15 +30,18 @@ func CLIPath() string { 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 - // index.js ships in the installed platform package - // (e.g. @github/copilot-linux-x64). - base := RepoPath("nodejs", "node_modules", "@github") - matches, _ := filepath.Glob(filepath.Join(base, "copilot-*", "index.js")) - if len(matches) > 0 { - cliPath = matches[0] - return + npm := "npm" + if runtime.GOOS == "windows" { + npm = "npm.cmd" + } + command := exec.Command(npm, "run", "--silent", "prepare:runtime", "--", "--print-path") + command.Dir = RepoPath("nodejs") + output, err := command.Output() + if err == nil { + candidate := strings.TrimSpace(string(output)) + if info, statErr := os.Stat(candidate); statErr == nil && !info.IsDir() { + cliPath = candidate + } } }) return cliPath diff --git a/go/samples/chat.go b/go/samples/chat.go index 2f34a243c9..85128a1b56 100644 --- a/go/samples/chat.go +++ b/go/samples/chat.go @@ -5,10 +5,10 @@ import ( "context" "fmt" "os" - "path/filepath" "strings" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/samples/internal/sampleutil" ) const blue = "\033[34m" @@ -16,7 +16,10 @@ const reset = "\033[0m" func main() { ctx := context.Background() - cliPath := filepath.Join("..", "..", "nodejs", "node_modules", "@github", "copilot", "index.js") + cliPath, err := sampleutil.CLIPath() + if err != nil { + panic(err) + } client := copilot.NewClient(&copilot.ClientOptions{Connection: copilot.StdioConnection{Path: cliPath}}) if err := client.Start(ctx); err != nil { panic(err) diff --git a/go/samples/internal/sampleutil/runtime.go b/go/samples/internal/sampleutil/runtime.go new file mode 100644 index 0000000000..5c51f0d8db --- /dev/null +++ b/go/samples/internal/sampleutil/runtime.go @@ -0,0 +1,48 @@ +package sampleutil + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// CLIPath resolves the pinned source-checkout runtime used by SDK samples. +func CLIPath() (string, error) { + if cliPath := os.Getenv("COPILOT_CLI_PATH"); cliPath != "" { + return cliPath, nil + } + + current, err := os.Getwd() + if err != nil { + return "", err + } + for { + nodeDir := filepath.Join(current, "nodejs") + if _, err := os.Stat(filepath.Join(nodeDir, "package.json")); err == nil { + command := exec.Command( + "node", + "node_modules/tsx/dist/cli.mjs", + "scripts/prepare-runtime.ts", + "--print-path", + ) + command.Dir = nodeDir + output, err := command.CombinedOutput() + if err != nil { + return "", fmt.Errorf("prepare pinned Copilot CLI: %w: %s", err, output) + } + cliPath := strings.TrimSpace(string(output)) + if info, err := os.Stat(cliPath); err != nil || info.IsDir() { + return "", fmt.Errorf("prepared Copilot CLI path is not a file: %q", cliPath) + } + return cliPath, nil + } + + parent := filepath.Dir(current) + if parent == current { + return "", fmt.Errorf("could not find nodejs/package.json; set COPILOT_CLI_PATH") + } + current = parent + } +} diff --git a/go/samples/manual_tool_resume/main.go b/go/samples/manual_tool_resume/main.go index a7391ff405..62806b7a78 100644 --- a/go/samples/manual_tool_resume/main.go +++ b/go/samples/manual_tool_resume/main.go @@ -4,11 +4,11 @@ import ( "context" "fmt" "os" - "path/filepath" "time" copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/rpc" + "github.com/github/copilot-sdk/go/samples/internal/sampleutil" ) const timeout = 2 * time.Minute @@ -32,7 +32,10 @@ func manualTool() copilot.Tool { } func newClient() *copilot.Client { - cliPath := filepath.Join("..", "..", "nodejs", "node_modules", "@github", "copilot", "index.js") + cliPath, err := sampleutil.CLIPath() + if err != nil { + panic(err) + } return copilot.NewClient(&copilot.ClientOptions{Connection: copilot.StdioConnection{Path: cliPath}}) } diff --git a/go/session.go b/go/session.go index 5d45d19ef2..32fed684d9 100644 --- a/go/session.go +++ b/go/session.go @@ -98,8 +98,10 @@ type Session struct { // eventCh serializes user event handler dispatch. dispatchEvent enqueues; // a single goroutine (processEvents) dequeues and invokes handlers in FIFO order. + // eventDone stops both sides without closing eventCh while a sender may be active. eventCh chan SessionEvent - closeOnce sync.Once // guards eventCh close so Disconnect is safe to call more than once + eventDone chan struct{} + closeOnce sync.Once // RPC provides typed session-scoped RPC methods. RPC *rpc.SessionRPC @@ -385,6 +387,7 @@ func newSession( toolHandlers: make(map[string]ToolHandler), commandHandlers: make(map[string]CommandHandler), eventCh: make(chan SessionEvent, 128), + eventDone: make(chan struct{}), RPC: rpc.NewSessionRPC(client, sessionID), } s.clientSessionAPIs.Canvas = newCanvasClientSessionAdapter(s) @@ -1391,14 +1394,10 @@ func (s *Session) dispatchEvent(event SessionEvent) { s.updateOpenCanvasesFromEvent(event) go s.handleBroadcastEvent(event) - // Send to the event channel in a closure with a recover guard. - // Disconnect closes eventCh, and in Go sending on a closed channel - // panics — there is no non-panicking send primitive. We only want - // to suppress that specific panic; other panics are not expected here. - func() { - defer func() { recover() }() - s.eventCh <- event - }() + select { + case s.eventCh <- event: + case <-s.eventDone: + } } // processEvents is the single consumer goroutine for the event channel. @@ -1406,7 +1405,14 @@ func (s *Session) dispatchEvent(event SessionEvent) { // handlers are recovered so that one misbehaving handler does not prevent // others from receiving the event. func (s *Session) processEvents() { - for event := range s.eventCh { + for { + var event SessionEvent + select { + case event = <-s.eventCh: + case <-s.eventDone: + return + } + s.handlerMutex.RLock() handlers := make([]SessionEventHandler, 0, len(s.handlers)) for _, h := range s.handlers { @@ -1427,6 +1433,10 @@ func (s *Session) processEvents() { } } +func (s *Session) stopEventProcessing() { + s.closeOnce.Do(func() { close(s.eventDone) }) +} + // handleBroadcastEvent handles broadcast request events by executing local handlers // and responding via RPC. This implements the protocol v3 broadcast model where tool // calls and permission requests are broadcast as session events to all clients. @@ -1729,7 +1739,7 @@ func (s *Session) GetEvents(ctx context.Context) ([]SessionEvent, error) { func (s *Session) Disconnect() error { _, err := s.client.Request(context.Background(), "session.destroy", sessionDestroyRequest{SessionID: s.SessionID}) - s.closeOnce.Do(func() { close(s.eventCh) }) + s.stopEventProcessing() s.releaseGitHubTokenProviderRegistration() // Clear handlers diff --git a/go/session_test.go b/go/session_test.go index 74e212c418..8d29cfc88f 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -18,21 +18,43 @@ import ( ) // newTestSession creates a session with an event channel and starts the consumer goroutine. -// Returns a cleanup function that closes the channel (stopping the consumer). +// Returns a cleanup function that stops the consumer. func newTestSession() (*Session, func()) { s := &Session{ handlers: make([]sessionHandler, 0), commandHandlers: make(map[string]CommandHandler), eventCh: make(chan SessionEvent, 128), + eventDone: make(chan struct{}), } go s.processEvents() - return s, func() { close(s.eventCh) } + return s, s.stopEventProcessing } func newTestEvent() SessionEvent { return SessionEvent{Data: &SessionIdleData{}} } +func TestDispatchEventReturnsAfterEventProcessingStops(t *testing.T) { + session := &Session{ + eventCh: make(chan SessionEvent), + eventDone: make(chan struct{}), + } + + dispatched := make(chan struct{}) + go func() { + session.dispatchEvent(newTestEvent()) + close(dispatched) + }() + + session.stopEventProcessing() + + select { + case <-dispatched: + case <-time.After(time.Second): + t.Fatal("dispatchEvent remained blocked after event processing stopped") + } +} + func ptr[T any](value T) *T { return &value } @@ -423,9 +445,10 @@ func TestSession_SendAndWaitSkipsAutopilotContinuationIdle(t *testing.T) { RPC: rpc.NewSessionRPC(client, "session-1"), handlers: make([]sessionHandler, 0), eventCh: make(chan SessionEvent, 8), + eventDone: make(chan struct{}), } go session.processEvents() - defer close(session.eventCh) + defer session.stopEventProcessing() resultCh := make(chan *SessionEvent, 1) go func() { diff --git a/go/test.sh b/go/test.sh index dfb7bac1dd..f5924b3e8d 100755 --- a/go/test.sh +++ b/go/test.sh @@ -15,17 +15,14 @@ fi # Determine COPILOT_CLI_PATH if [ -z "$COPILOT_CLI_PATH" ]; then - # Try to find it relative to the SDK. 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. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - POTENTIAL_PATH="$(ls "$SCRIPT_DIR"/../nodejs/node_modules/@github/copilot-*/index.js 2>/dev/null | head -n1)" - if [ -n "$POTENTIAL_PATH" ] && [ -f "$POTENTIAL_PATH" ]; then + if POTENTIAL_PATH="$(cd "$SCRIPT_DIR/../nodejs" && npm run --silent prepare:runtime -- --print-path)" && + [ -n "$POTENTIAL_PATH" ] && [ -f "$POTENTIAL_PATH" ]; then export COPILOT_CLI_PATH="$POTENTIAL_PATH" echo "📍 Auto-detected CLI path: $COPILOT_CLI_PATH" else echo "❌ COPILOT_CLI_PATH environment variable not set" - echo " Run: export COPILOT_CLI_PATH=/path/to/dist-cli/index.js" + echo " Run: export COPILOT_CLI_PATH=/path/to/copilot" exit 1 fi fi diff --git a/java/README.md b/java/README.md index f01911c159..356c1c5c1d 100644 --- a/java/README.md +++ b/java/README.md @@ -548,9 +548,9 @@ mvn jacoco:prepare-agent@wire-up-coverage-instrumentation antrun:run@print-test- #### Development Setup for native embedding -Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js and npm in addition to JDK 25 and Maven because `copilot-native/scripts/fetch-native.mjs` retrieves the pinned npm runtime package. +Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js in addition to JDK 25 and Maven because `copilot-native/scripts/fetch-native.mjs` retrieves the pinned runtime package from the corresponding GitHub release. -On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On Windows x64, Windows ARM64, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, fetches the pinned `@github/copilot-` package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Ensure npm can authenticate to the package registry before running the build. +On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On Windows x64, Windows ARM64, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, fetches the pinned platform package from the corresponding `github/copilot-cli` release during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Before opting in, validate that Node.js reports glibc for the build host: diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index c03be9909e..8dff6c4ad6 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -31,14 +31,13 @@ ${project.basedir}/../.. ${project.build.directory}/native-staging false @@ -60,11 +59,10 @@ org.codehaus.mojo diff --git a/java/copilot-native/scripts/fetch-native.mjs b/java/copilot-native/scripts/fetch-native.mjs index 4ee91b6aa6..49d2180a73 100644 --- a/java/copilot-native/scripts/fetch-native.mjs +++ b/java/copilot-native/scripts/fetch-native.mjs @@ -6,10 +6,9 @@ * Downloads the native runtime artifacts for one platform classifier. * * Steps: - * 1. Read the pinned version and the SHA-512 `integrity` value for - * `@github/copilot-` from `nodejs/package-lock.json`. - * 2. `npm pack` that exact version into the staging directory. - * 3. Verify the downloaded tarball against the `integrity` value. + * 1. Read the pinned version from `nodejs/package.json`. + * 2. Download the platform npm tarball and `SHA256SUMS.txt` from the matching release. + * 3. Verify the downloaded tarball against the release checksum. * 4. Stage the hostless runtime tree, flattening the selected prebuild directory * beside the package's retained top-level runtime assets. * 5. Write an inventory consumed by the SDK's generic classpath extractor. @@ -29,18 +28,15 @@ const excludedTopLevel = new Set([ 'changelog.json', 'copilot', 'copilot.exe', - 'copilot-sdk', 'foundry-local-sdk', 'index.js', 'LICENSE.md', 'napi-oop-runtime', 'npm-loader.js', 'package.json', - 'preloads', 'pvrecorder', 'queries', 'README.md', - 'sdk', 'sea-loader.js', 'webview', ]); @@ -52,19 +48,12 @@ if (!repoRoot || !stagingDir || !classifier) { process.exit(1); } -const lockPath = path.join(repoRoot, 'nodejs', 'package-lock.json'); +const packagePath = path.join(repoRoot, 'nodejs', 'package.json'); const packageName = `@github/copilot-${classifier}`; -const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); -const entry = lock.packages?.[`node_modules/${packageName}`]; - -if (!entry?.version || !entry?.integrity) { - console.error(`Could not find version/integrity for ${packageName} in ${lockPath}`); - process.exit(1); -} - -const { version, integrity } = entry; -if (!integrity.startsWith('sha512-')) { - console.error(`Unsupported integrity algorithm for ${packageName}: ${integrity}`); +const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); +const version = packageJson.copilotCliVersion; +if (!version) { + console.error(`Could not find copilotCliVersion in ${packagePath}`); process.exit(1); } @@ -77,7 +66,7 @@ const wrapperPath = path.join(resourceDir, wrapperFilename); const inventoryPath = path.join(resourceDir, 'runtime-assets.list'); const platformPropertiesPath = path.join(resourceDir, 'platform.properties'); const expectedPlatformProperties = `classifier=${classifier}\nversion=${version}\n`; -const stagingSchema = 'hostless-runtime-v2'; +const stagingSchema = 'hostless-runtime-v3'; const stampPath = path.join(outDir, '.version'); // Idempotence: skip the download only when every required staged artifact @@ -92,14 +81,12 @@ if ( const stampLines = fs.readFileSync(stampPath, 'utf8').trim().split('\n'); const stampSchema = stampLines[0] || ''; const stampVersion = stampLines[1] || ''; - const stampIntegrity = stampLines[2] || ''; const stampTreeDigest = stampLines[3] || ''; const currentTreeDigest = digestTree(resourceDir); const currentPlatformProperties = fs.readFileSync(platformPropertiesPath, 'utf8'); if ( stampSchema === stagingSchema && stampVersion === version && - stampIntegrity === integrity && stampTreeDigest === currentTreeDigest && currentPlatformProperties === expectedPlatformProperties ) { @@ -112,24 +99,36 @@ fs.rmSync(outDir, { recursive: true, force: true }); fs.mkdirSync(resourceDir, { recursive: true }); console.log(`Downloading ${packageName}@${version} ...`); -const packOutput = execFileSync('npm', ['pack', `${packageName}@${version}`, '--pack-destination', outDir], { - encoding: 'utf8', - shell: process.platform === 'win32', -}); -const tarballName = packOutput.trim().split('\n').pop().trim(); -const tarballPath = path.join(outDir, tarballName); - -const actual = `sha512-${createHash('sha512').update(fs.readFileSync(tarballPath)).digest('base64')}`; -if (actual !== integrity) { - console.error(`Integrity verification failed for ${tarballPath}`); - console.error(` expected: ${integrity}`); +const assetName = `github-copilot-${version}-${classifier}.tgz`; +const releaseBase = ( + process.env.COPILOT_CLI_DOWNLOAD_BASE_URL ?? + 'https://github.com/github/copilot-cli/releases/download' +).replace(/\/+$/, ''); +let archive; +let expectedHash; +if (process.env.COPILOT_CLI_RELEASE_TARBALL) { + archive = fs.readFileSync(process.env.COPILOT_CLI_RELEASE_TARBALL); + expectedHash = process.env.COPILOT_CLI_RELEASE_SHA256; +} else { + const releaseUrl = `${releaseBase}/v${version}`; + const checksums = (await download(`${releaseUrl}/SHA256SUMS.txt`)).toString('utf8'); + expectedHash = findChecksum(checksums, assetName); + archive = await download(`${releaseUrl}/${assetName}`); +} +if (!expectedHash || !/^[a-fA-F0-9]{64}$/.test(expectedHash)) { + throw new Error(`Missing or invalid SHA-256 for ${assetName}`); +} +const actual = createHash('sha256').update(archive).digest('hex'); +if (actual !== expectedHash.toLowerCase()) { + console.error(`Integrity verification failed for ${assetName}`); + console.error(` expected: ${expectedHash}`); console.error(` actual: ${actual}`); process.exit(1); } -console.log(`Integrity verified (${integrity.slice(0, 20)}...).`); +console.log(`Integrity verified (${expectedHash.slice(0, 20)}...).`); const inventory = []; -const members = execFileSync('tar', ['-tzf', tarballPath], { encoding: 'utf8' }) +const members = execFileSync('tar', ['-tzf', '-'], { encoding: 'utf8', input: archive }) .split(/\r?\n/) .filter(Boolean); for (const member of members) { @@ -137,15 +136,19 @@ for (const member of members) { if (destinationRelative === null) { continue; } - const listing = execFileSync('tar', ['-tvzf', tarballPath, member], { encoding: 'utf8' }).trim(); + const listing = execFileSync('tar', ['-tvzf', '-', member], { + encoding: 'utf8', + input: archive, + }).trim(); if (listing.startsWith('d')) { continue; } if (!listing.startsWith('-')) { throw new Error(`Unsupported runtime package entry: ${member}`); } - const content = execFileSync('tar', ['-xOzf', tarballPath, member], { + const content = execFileSync('tar', ['-xOzf', '-', member], { encoding: null, + input: archive, maxBuffer: 512 * 1024 * 1024, }); const destination = path.resolve(resourceDir, destinationRelative); @@ -162,14 +165,12 @@ for (const member of members) { inventory.sort(); fs.writeFileSync(inventoryPath, `${inventory.join('\n')}\n`); -fs.rmSync(tarballPath, { force: true }); - if (!fs.existsSync(runtimePath) || !fs.existsSync(wrapperPath)) { throw new Error(`Package ${packageName}@${version} is missing the runtime wrapper pair`); } fs.writeFileSync(platformPropertiesPath, expectedPlatformProperties); const treeDigest = digestTree(resourceDir); -fs.writeFileSync(stampPath, `${stagingSchema}\n${version}\n${integrity}\n${treeDigest}\n`); +fs.writeFileSync(stampPath, `${stagingSchema}\n${version}\n${expectedHash}\n${treeDigest}\n`); console.log(`Staged ${runtimePath}`); @@ -224,3 +225,37 @@ function digestTree(directory) { } return `sha512-${hash.digest('base64')}`; } + +async function download(url) { + let lastError; + for (let attempt = 0; attempt < 3; attempt++) { + try { + // lgtm[js/file-access-to-http] The repository-pinned CLI version intentionally selects the release asset. + const response = await fetch(url); + if (response.ok) { + return Buffer.from(await response.arrayBuffer()); + } + await response.body?.cancel(); + lastError = new Error(`${response.status} ${response.statusText}`); + if (response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429) { + break; + } + } catch (error) { + lastError = error; + } + if (attempt < 2) { + await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000)); + } + } + throw new Error(`Failed to download ${url}: ${lastError}`); +} + +function findChecksum(checksums, assetName) { + for (const line of checksums.split(/\r?\n/)) { + const [hash, name] = line.trim().split(/\s+/, 2); + if (name?.replace(/^\*/, '') === assetName && /^[a-fA-F0-9]{64}$/.test(hash)) { + return hash.toLowerCase(); + } + } + throw new Error(`SHA256SUMS.txt does not contain ${assetName}`); +} diff --git a/java/copilot-native/scripts/fetch-native.test.mjs b/java/copilot-native/scripts/fetch-native.test.mjs index 582ffa397f..3213d416f0 100644 --- a/java/copilot-native/scripts/fetch-native.test.mjs +++ b/java/copilot-native/scripts/fetch-native.test.mjs @@ -12,10 +12,10 @@ import { fileURLToPath } from 'node:url'; import test from 'node:test'; const version = '1.0.79'; -const integrity = 'sha512-test-integrity'; +const checksum = '0'.repeat(64); const runtimeContent = 'runtime content'; const wrapperContent = 'wrapper content'; -const stagingSchema = 'hostless-runtime-v2'; +const stagingSchema = 'hostless-runtime-v3'; const scriptPath = fileURLToPath(new URL('./fetch-native.mjs', import.meta.url)); for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64', 'darwin-arm64']) { @@ -26,7 +26,6 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /already staged/); - assert.equal(fs.existsSync(fixture.npmMarkerPath), false); }); test(`${classifier}: missing runtime wrapper does not use incremental fast path`, (t) => { @@ -38,10 +37,10 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' assertRestagingAttempted(fixture, result); }); - test(`${classifier}: legacy staging schema does not use incremental fast path`, (t) => { + test(`${classifier}: v2 staging schema does not use incremental fast path`, (t) => { const fixture = createFixture(t, classifier); const stampPath = path.join(fixture.stagingDir, classifier, '.version'); - fs.writeFileSync(stampPath, fs.readFileSync(stampPath, 'utf8').replace(stagingSchema, 'hostless-runtime-v1')); + fs.writeFileSync(stampPath, fs.readFileSync(stampPath, 'utf8').replace(stagingSchema, 'hostless-runtime-v2')); const result = runScript(fixture); @@ -73,7 +72,6 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /already staged/); - assert.equal(fs.existsSync(fixture.npmMarkerPath), false); }); } @@ -95,25 +93,17 @@ test('stages retained package assets and excludes CLI-only content', (t) => { fs.writeFileSync(path.join(packageRoot, 'README.md'), 'excluded'); const tarball = path.join(fixture.repoRoot, 'fixture.tgz'); execFileSync('tar', ['-czf', tarball, '-C', path.dirname(packageRoot), 'package']); - const packageIntegrity = digest(fs.readFileSync(tarball)); + const packageChecksum = createHash('sha256').update(fs.readFileSync(tarball)).digest('hex'); fs.writeFileSync( - path.join(fixture.repoRoot, 'nodejs', 'package-lock.json'), - JSON.stringify({ - packages: { - [`node_modules/@github/copilot-${classifier}`]: { version, integrity: packageIntegrity }, - }, - }), + path.join(fixture.repoRoot, 'nodejs', 'package.json'), + JSON.stringify({ copilotCliVersion: version }), ); - const fakeNpmPath = path.join(fixture.fakeBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); - const fakeNpm = - process.platform === 'win32' - ? '@copy "%FETCH_NATIVE_TARBALL%" "%4\\fixture.tgz" >nul\r\n@echo fixture.tgz\r\n' - : '#!/bin/sh\ncp "$FETCH_NATIVE_TARBALL" "$4/fixture.tgz"\nprintf "fixture.tgz\\n"\n'; - fs.writeFileSync(fakeNpmPath, fakeNpm); - fs.chmodSync(fakeNpmPath, 0o755); fs.rmSync(path.join(fixture.stagingDir, classifier), { recursive: true, force: true }); - const result = runScript(fixture, { FETCH_NATIVE_TARBALL: tarball }); + const result = runScript(fixture, { + COPILOT_CLI_RELEASE_TARBALL: tarball, + COPILOT_CLI_RELEASE_SHA256: packageChecksum, + }); assert.equal(result.status, 0, result.stderr); const resourceDir = path.join(fixture.stagingDir, classifier, 'native', classifier); @@ -133,19 +123,12 @@ function createFixture(t, classifier) { const repoRoot = path.join(root, 'repo'); const stagingDir = path.join(root, 'staging'); const resourceDir = path.join(stagingDir, classifier, 'native', classifier); - const fakeBinDir = path.join(root, 'bin'); - const npmMarkerPath = path.join(root, 'npm-invoked'); fs.mkdirSync(path.join(repoRoot, 'nodejs'), { recursive: true }); fs.mkdirSync(resourceDir, { recursive: true }); - fs.mkdirSync(fakeBinDir); fs.writeFileSync( - path.join(repoRoot, 'nodejs', 'package-lock.json'), - JSON.stringify({ - packages: { - [`node_modules/@github/copilot-${classifier}`]: { version, integrity }, - }, - }), + path.join(repoRoot, 'nodejs', 'package.json'), + JSON.stringify({ copilotCliVersion: version }), ); const runtimePath = path.join(resourceDir, 'runtime.node'); @@ -167,23 +150,14 @@ function createFixture(t, classifier) { fs.writeFileSync(platformPropertiesPath, `classifier=${classifier}\nversion=${version}\n`); fs.writeFileSync( path.join(stagingDir, classifier, '.version'), - `${stagingSchema}\n${version}\n${integrity}\n${digestTree(resourceDir)}\n`, + `${stagingSchema}\n${version}\n${checksum}\n${digestTree(resourceDir)}\n`, ); - const fakeNpmPath = path.join(fakeBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); - if (process.platform === 'win32') { - fs.writeFileSync(fakeNpmPath, '@echo off\r\n> "%FETCH_NATIVE_NPM_MARKER%" echo invoked\r\nexit /b 42\r\n'); - } else { - fs.writeFileSync(fakeNpmPath, '#!/bin/sh\nprintf invoked > "$FETCH_NATIVE_NPM_MARKER"\nexit 42\n'); - fs.chmodSync(fakeNpmPath, 0o755); - } - return { + root, classifier, repoRoot, stagingDir, - fakeBinDir, - npmMarkerPath, runtimePath, wrapperPath, ripgrepPath, @@ -196,16 +170,15 @@ function runScript(fixture, extraEnv = {}) { encoding: 'utf8', env: { ...process.env, - PATH: `${fixture.fakeBinDir}${path.delimiter}${process.env.PATH}`, - FETCH_NATIVE_NPM_MARKER: fixture.npmMarkerPath, + COPILOT_CLI_RELEASE_TARBALL: path.join(fixture.root, 'missing.tgz'), + COPILOT_CLI_RELEASE_SHA256: checksum, ...extraEnv, }, }); } function assertRestagingAttempted(fixture, result) { - assert.notEqual(result.status, 0, 'The fake npm command should make restaging fail'); - assert.equal(fs.readFileSync(fixture.npmMarkerPath, 'utf8').trim(), 'invoked'); + assert.notEqual(result.status, 0, 'The unavailable release should make restaging fail'); } function digestTree(directory) { diff --git a/java/copilot-native/scripts/validate-native-artifact.mjs b/java/copilot-native/scripts/validate-native-artifact.mjs index 9af4ffd772..466fa260a4 100644 --- a/java/copilot-native/scripts/validate-native-artifact.mjs +++ b/java/copilot-native/scripts/validate-native-artifact.mjs @@ -119,22 +119,19 @@ export function validateSha256Manifest({ } export function readPinnedNativeVersion(repoRoot, classifier) { - const packageName = `@github/copilot-${classifier}`; - const lockPath = path.join(repoRoot, "nodejs", "package-lock.json"); - let lock; + const packagePath = path.join(repoRoot, "nodejs", "package.json"); + let packageJson; try { - lock = JSON.parse(fs.readFileSync(lockPath, "utf8")); + packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); } catch (error) { throw new Error( - `Could not read pinned ${packageName} version from ${lockPath}: ${error.message}`, + `Could not read pinned Copilot CLI version from ${packagePath}: ${error.message}`, ); } - const version = lock.packages?.[`node_modules/${packageName}`]?.version; + const version = packageJson.copilotCliVersion; if (!version) { - throw new Error( - `Could not find pinned ${packageName} version in ${lockPath}`, - ); + throw new Error(`Could not find copilotCliVersion in ${packagePath}`); } return version; } diff --git a/java/copilot-native/scripts/validate-native-artifact.test.mjs b/java/copilot-native/scripts/validate-native-artifact.test.mjs index 25851bb575..c07695a484 100644 --- a/java/copilot-native/scripts/validate-native-artifact.test.mjs +++ b/java/copilot-native/scripts/validate-native-artifact.test.mjs @@ -67,7 +67,7 @@ test("accepts a matching, complete Windows ARM64 classifier", (t) => { }), { classifier: windowsArm64Classifier, - nativeVersion: "9.8.10", + nativeVersion: "9.8.7", sha256: undefined, }, ); @@ -94,7 +94,7 @@ test("accepts a matching, complete Darwin classifier", (t) => { }), { classifier: darwinClassifier, - nativeVersion: "9.8.8", + nativeVersion: "9.8.7", sha256: undefined, }, ); @@ -121,7 +121,7 @@ test("accepts a matching, complete Linux ARM64 classifier", (t) => { }), { classifier: linuxArm64Classifier, - nativeVersion: "9.8.9", + nativeVersion: "9.8.7", sha256: undefined, }, ); @@ -248,7 +248,7 @@ test("rejects Windows resources in a Linux classifier", (t) => { ["native/linux-x64/copilot-runtime", "runtime wrapper"], [ "native/linux-x64/platform.properties", - "classifier=linux-x64\nversion=9.8.6\n", + "classifier=linux-x64\nversion=9.8.7\n", ], ["native/win32-x64/runtime.node", "wrong platform"], ]); @@ -462,7 +462,7 @@ test("local publication validation rejects cross-classifier contamination", (t) ["native/linux-x64/copilot-runtime", "runtime wrapper"], [ "native/linux-x64/platform.properties", - "classifier=linux-x64\nversion=9.8.6\n", + "classifier=linux-x64\nversion=9.8.7\n", ], ["native/win32-x64/runtime.node", "wrong platform"], ], @@ -537,16 +537,8 @@ function createFixture(t) { const repoRoot = path.join(root, "repo"); fs.mkdirSync(path.join(repoRoot, "nodejs"), { recursive: true }); fs.writeFileSync( - path.join(repoRoot, "nodejs", "package-lock.json"), - JSON.stringify({ - packages: { - "node_modules/@github/copilot-win32-x64": { version: "9.8.7" }, - "node_modules/@github/copilot-win32-arm64": { version: "9.8.10" }, - "node_modules/@github/copilot-linux-x64": { version: "9.8.6" }, - "node_modules/@github/copilot-linux-arm64": { version: "9.8.9" }, - "node_modules/@github/copilot-darwin-arm64": { version: "9.8.8" }, - }, - }), + path.join(repoRoot, "nodejs", "package.json"), + JSON.stringify({ copilotCliVersion: "9.8.7" }), ); return { diff --git a/java/docs/adr/adr-007-native-bundling-strategy.md b/java/docs/adr/adr-007-native-bundling-strategy.md index 1540829366..3c13d451cf 100644 --- a/java/docs/adr/adr-007-native-bundling-strategy.md +++ b/java/docs/adr/adr-007-native-bundling-strategy.md @@ -361,7 +361,7 @@ The pattern follows DJL's `LibUtils.loadLibrary()` approach: detect the platform 2. Locates the matching `runtime.node` binary on the classpath (via `getResourceAsStream` from the classifier JAR). 3. Extracts `runtime.node` and the transitional CLI entrypoint into `~/.copilot/runtime-cache/` if valid cached files are not already present. 4. Loads it via [JNA](#references) using the C ABI entry points, per the [binding technology decision](#binding-technology-jna-over-panama-ffm) above. The JNA-specific code is confined behind an internal binding interface to preserve a future FFM migration path. -* A validated supported-host profile fetches the pinned matching `@github/copilot-` npm package, verifies its SHA-512 integrity from `nodejs/package-lock.json`, and packages the version-matched runtime and CLI files. +* A validated supported-host profile fetches the matching platform tarball from the pinned `github/copilot-cli` release, verifies its release SHA-256, and packages the version-matched runtime files. * The current release work publishes the `linux-x64`, `linux-arm64`, `win32-x64`, `win32-arm64`, and `darwin-arm64` classifiers. The planned classifier set expands to the other detected platforms. * Adding an implemented platform requires validated host activation, a profile that supplies the classifier and platform CLI filename, and lifecycle bindings for the shared host validation, fetch, script test, package, and verification executions. * `cli-native.node` is not bundled. It provides terminal UI features that are irrelevant to the Java SDK's programmatic API surface. diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index dd22744f48..4f6ae9771e 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -42,20 +42,18 @@ ${project.basedir}/../.. ${copilot.sdk.root}/test - ${copilot.sdk.root}/nodejs/node_modules/@github/copilot/npm-loader.js + false + install-nodejs-cli-dependencies generate-test-resources @@ -249,9 +242,8 @@ ${copilot.cli.path} @@ -281,10 +273,9 @@ ${copilot.cli.path} diff --git a/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java b/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java index cb302a8cd2..a6bec65d2c 100644 --- a/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java +++ b/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java @@ -596,53 +596,15 @@ private static String getCliPath(Path repoRoot) throws IOException { return envPath; } - // Try test harness platform-specific binary (preferred as it has correct - // version) - String os = System.getProperty("os.name").toLowerCase(); - String arch = System.getProperty("os.arch").toLowerCase(); - String platform = os.contains("mac") ? "darwin" : os.contains("win") ? "win32" : "linux"; - String cpuArch = arch.contains("aarch64") || arch.contains("arm64") ? "arm64" : "x64"; - Path platformBinary = repoRoot - .resolve("test/harness/node_modules/@github/copilot-" + platform + "-" + cpuArch + "/copilot"); - if (os.contains("win")) { - platformBinary = repoRoot - .resolve("test/harness/node_modules/@github/copilot-" + platform + "-" + cpuArch + "/copilot.exe"); - } - if (Files.exists(platformBinary)) { - return platformBinary.toString(); - } - - // Try test harness npm-loader.js - Path harnessCliPath = repoRoot.resolve("test/harness/node_modules/@github/copilot/npm-loader.js"); - if (Files.exists(harnessCliPath)) { - return harnessCliPath.toString(); - } - - // Try nodejs installation. As of CLI 1.0.64-1 the @github/copilot package - // is a thin loader; the runnable index.js ships in the installed - // platform-specific package (e.g. @github/copilot-linux-x64). Exactly one - // is installed. Running index.js under Node.js is the documented preferred - // entry point and matches the Go, Python, Rust, and .NET test harnesses. - Path githubModules = repoRoot.resolve("nodejs/node_modules/@github"); - if (Files.isDirectory(githubModules)) { - try (var modules = Files.newDirectoryStream(githubModules, "copilot-*")) { - for (Path module : modules) { - Path indexJs = module.resolve("index.js"); - if (Files.exists(indexJs)) { - return indexJs.toString(); - } - } + try { + return TestUtil.preparePinnedCli(repoRoot); + } catch (Exception e) { + String copilotInPath = findCopilotInPath(); + if (copilotInPath != null) { + return copilotInPath; } + throw new IOException("The pinned CLI could not be prepared and no CLI was found on PATH.", e); } - - // Fallback: try to find 'copilot' in PATH - String copilotInPath = findCopilotInPath(); - if (copilotInPath != null) { - return copilotInPath; - } - - throw new IOException("CLI not found. Either install 'copilot' globally, set COPILOT_CLI_PATH, " - + "or run 'npm install' in the nodejs directory or test/harness directory."); } private static String findCopilotInPath() { diff --git a/java/sdk/src/test/java/com/github/copilot/TestUtil.java b/java/sdk/src/test/java/com/github/copilot/TestUtil.java index 23bb53e493..8e90c2f48f 100644 --- a/java/sdk/src/test/java/com/github/copilot/TestUtil.java +++ b/java/sdk/src/test/java/com/github/copilot/TestUtil.java @@ -5,9 +5,12 @@ package com.github.copilot; import java.io.BufferedReader; +import java.io.File; import java.io.InputStreamReader; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.regex.Pattern; /** * Shared test utilities for locating the Copilot CLI binary and other @@ -37,10 +40,9 @@ public static String tempPath(String filename) { * Resolution order: *
    *
  1. Use the {@code COPILOT_CLI_PATH} environment variable when set.
  2. + *
  3. Prepare the release pinned by {@code nodejs/package.json}.
  4. *
  5. Otherwise search the system PATH using {@code where.exe} (Windows) or * {@code which} (Linux/macOS).
  6. - *
  7. Walk parent directories looking for - * {@code nodejs/node_modules/@github/copilot/npm-loader.js}.
  8. *
* *

@@ -60,43 +62,54 @@ static String findCliPath() { return envPath; } - String copilotInPath = findCopilotInPath(); - if (copilotInPath != null) { - return copilotInPath; - } - - // Walk parent directories looking for the CLI in the test harness or nodejs - // installation. Mirrors the resolution order in E2ETestContext.getCliPath(). - String os = System.getProperty("os.name").toLowerCase(); - String arch = System.getProperty("os.arch").toLowerCase(); - String platform = os.contains("mac") ? "darwin" : os.contains("win") ? "win32" : "linux"; - String cpuArch = arch.contains("aarch64") || arch.contains("arm64") ? "arm64" : "x64"; - String binaryName = os.contains("win") ? "copilot.exe" : "copilot"; - Path current = Paths.get(System.getProperty("user.dir")); while (current != null) { - // Test harness platform-specific binary - Path platformBinary = current.resolve( - "test/harness/node_modules/@github/copilot-" + platform + "-" + cpuArch + "/" + binaryName); - if (platformBinary.toFile().exists()) { - return platformBinary.toString(); + if (current.resolve("nodejs/package.json").toFile().exists()) { + try { + return preparePinnedCli(current); + } catch (Exception preparationFailed) { + break; + } } + current = current.getParent(); + } - // Test harness npm-loader.js - Path npmLoader = current.resolve("test/harness/node_modules/@github/copilot/npm-loader.js"); - if (npmLoader.toFile().exists()) { - return npmLoader.toString(); - } + return findCopilotInPath(); + } - // nodejs installation (thin loader; resolves the platform-specific - // CLI package internally) - Path cliPath = current.resolve("nodejs/node_modules/@github/copilot/npm-loader.js"); - if (cliPath.toFile().exists()) { - return cliPath.toString(); - } - current = current.getParent(); + static String preparePinnedCli(Path repoRoot) throws Exception { + var nodePath = findExecutableInPath("node"); + if (nodePath == null) { + throw new IllegalStateException("Node.js was not found in PATH"); + } + var process = new ProcessBuilder(nodePath, "node_modules/tsx/dist/cli.mjs", "scripts/prepare-runtime.ts", + "--print-path").directory(repoRoot.resolve("nodejs").toFile()).redirectErrorStream(true).start(); + String output; + try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + output = reader.lines().reduce((first, second) -> second).orElse("").trim(); + } + int exitCode = process.waitFor(); + if (exitCode != 0 || output.isEmpty()) { + throw new IllegalStateException("Failed to prepare the pinned Copilot CLI: " + output); + } + return output; + } + + private static String findExecutableInPath(String name) { + var pathValue = System.getenv("PATH"); + if (pathValue == null || pathValue.isEmpty()) { + return null; } + var windows = System.getProperty("os.name").toLowerCase().contains("win"); + var fileName = windows ? name + ".exe" : name; + for (var directory : pathValue.split(Pattern.quote(File.pathSeparator))) { + var unquotedDirectory = directory.replaceAll("^\"|\"$", ""); + var candidate = Paths.get(unquotedDirectory, fileName).toAbsolutePath().normalize(); + if (Files.isRegularFile(candidate) && (windows || Files.isExecutable(candidate))) { + return candidate.toString(); + } + } return null; } diff --git a/nodejs/README.md b/nodejs/README.md index 57a8bf484b..3a2a536e65 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -8,6 +8,20 @@ To use the SDK, you'll need: - Node.js ^20.19.0 or >=22.12.0 +The SDK uses an optional `@github/copilot-sdk-` package containing the +Copilot CLI runtime for the host platform. These packages are built from +verified `github/copilot-cli` release assets when the SDK is published, so +starting the SDK performs no runtime download. Set `COPILOT_CLI_PATH` to use an +existing installation instead. + +The checked-in release pin is `copilotCliVersion` in `package.json`. Run +`npm run set:cli-version -- ` to update it and regenerate the compiled +metadata in `src/cliVersion.ts`. Packaging verifies release assets against the +release's `SHA256SUMS.txt`. + +`npm run pack:release` builds the main package and all platform packages. Set +`COPILOT_CLI_DOWNLOAD_BASE_URL` to use a release mirror while packaging. + ## Installation ```bash @@ -22,6 +36,7 @@ Try the interactive chat sample (from the repo root): cd nodejs npm ci npm run build +export COPILOT_CLI_PATH="$(npm run --silent prepare:runtime -- --print-path)" cd samples npm install npm start @@ -936,15 +951,15 @@ const session = await client.createSession({ The handler must return one of the `PermissionDecision` shapes (or `{ kind: "no-result" }`). Approval scopes are present-tense — they describe the decision to apply, not the outcome reported back on session events: -| Kind | Meaning | Extra fields | -| ------------------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -| `"approve-once"` | Allow this single request | — | -| `"approve-for-session"` | Allow this request and remember the approval for the rest of the session | `approval?` (rule to remember), `domain?` (for URL approvals) | -| `"approve-for-location"` | Allow this request and persist the approval for this project location (git root or cwd) | `approval` (rule to persist), `locationKey` (location to persist under) | -| `"approve-permanently"` | Allow this request and persist the approval across sessions (currently used for URL domains) | `domain` (URL domain to approve) | -| `"reject"` | Deny the request | `feedback?` (optional string surfaced to the agent) | -| `"user-not-available"` | Deny the request because no user is available to confirm it | — | -| `"no-result"` | Suppress this SDK client's response so another connected client can answer the pending request | — | +| Kind | Meaning | Extra fields | +| ------------------------ | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `"approve-once"` | Allow this single request | — | +| `"approve-for-session"` | Allow this request and remember the approval for the rest of the session | `approval?` (rule to remember), `domain?` (for URL approvals) | +| `"approve-for-location"` | Allow this request and persist the approval for this project location (git root or cwd) | `approval` (rule to persist), `locationKey` (location to persist under) | +| `"approve-permanently"` | Allow this request and persist the approval across sessions (currently used for URL domains) | `domain` (URL domain to approve) | +| `"reject"` | Deny the request | `feedback?` (optional string surfaced to the agent) | +| `"user-not-available"` | Deny the request because no user is available to confirm it | — | +| `"no-result"` | Suppress this SDK client's response so another connected client can answer the pending request | — | ### Resuming Sessions diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 2dd065c1bb..a10153a1d3 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-3", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -29,6 +28,7 @@ "quicktype-core": "^23.2.6", "rimraf": "^6.1.2", "semver": "^7.7.3", + "tar": "^7.5.22", "tsx": "^4.20.6", "typescript": "^5.0.0", "vitest": "^4.0.18", @@ -57,7 +57,7 @@ }, "node_modules/@emnapi/core": { "version": "1.10.0", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "integrity": "sha1-OAzMjyQS6iLR2XLff47iOjucdGc=", "dev": true, "license": "MIT", "optional": true, @@ -68,7 +68,7 @@ }, "node_modules/@emnapi/runtime": { "version": "1.10.0", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "integrity": "sha1-SyYMDTU0IE6YxhELjbGph9JuyHw=", "dev": true, "license": "MIT", "optional": true, @@ -78,7 +78,7 @@ }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "integrity": "sha1-KP7SGhuhznl8RKBwq8lNQvOuhUg=", "dev": true, "license": "MIT", "optional": true, @@ -88,7 +88,7 @@ }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "integrity": "sha1-egGo0uwvuy2seK2tCbD6eB5Agr4=", "cpu": [ "ppc64" ], @@ -104,7 +104,7 @@ }, "node_modules/@esbuild/android-arm": { "version": "0.28.1", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "integrity": "sha1-cEvSl95tdi3lTqu+r79V9nVqvi8=", "cpu": [ "arm" ], @@ -120,7 +120,7 @@ }, "node_modules/@esbuild/android-arm64": { "version": "0.28.1", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "integrity": "sha1-tUCifRTkr9BYSWpNvsTT9BTbEQo=", "cpu": [ "arm64" ], @@ -136,7 +136,7 @@ }, "node_modules/@esbuild/android-x64": { "version": "0.28.1", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "integrity": "sha1-0csWbTSw+/D+irRgpVlPJKN4cB4=", "cpu": [ "x64" ], @@ -168,7 +168,7 @@ }, "node_modules/@esbuild/darwin-x64": { "version": "0.28.1", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "integrity": "sha1-ZVVqQyoeTXIDLYIYwZMvzKGkl3I=", "cpu": [ "x64" ], @@ -184,7 +184,7 @@ }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.28.1", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "integrity": "sha1-LmHgWS+QMNfj2uGO4l68U1kYrvY=", "cpu": [ "arm64" ], @@ -200,7 +200,7 @@ }, "node_modules/@esbuild/freebsd-x64": { "version": "0.28.1", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "integrity": "sha1-yV7CiZWe+AecTcqBeh4sS+Zrm9M=", "cpu": [ "x64" ], @@ -216,7 +216,7 @@ }, "node_modules/@esbuild/linux-arm": { "version": "0.28.1", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "integrity": "sha1-wJoPZ5F1kqwN6JKpvk04FN69Kmw=", "cpu": [ "arm" ], @@ -232,7 +232,7 @@ }, "node_modules/@esbuild/linux-arm64": { "version": "0.28.1", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "integrity": "sha1-QLIhdd2gYYLz7oFBGGxf8wTEpxc=", "cpu": [ "arm64" ], @@ -248,7 +248,7 @@ }, "node_modules/@esbuild/linux-ia32": { "version": "0.28.1", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "integrity": "sha1-pYD5xnZ5eDOJHlGfx6EzfIr9jbM=", "cpu": [ "ia32" ], @@ -264,7 +264,7 @@ }, "node_modules/@esbuild/linux-loong64": { "version": "0.28.1", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "integrity": "sha1-RkUs8yHcf56Rwvp4Cla7Vuec1os=", "cpu": [ "loong64" ], @@ -280,7 +280,7 @@ }, "node_modules/@esbuild/linux-mips64el": { "version": "0.28.1", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "integrity": "sha1-QhGzGE3WYI9T3LIuOfXTTuCIUsg=", "cpu": [ "mips64el" ], @@ -296,7 +296,7 @@ }, "node_modules/@esbuild/linux-ppc64": { "version": "0.28.1", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "integrity": "sha1-aXhXwqYcubC2u2ZS5AwdxeHKjl0=", "cpu": [ "ppc64" ], @@ -312,7 +312,7 @@ }, "node_modules/@esbuild/linux-riscv64": { "version": "0.28.1", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "integrity": "sha1-0ZKUPrFGpArExkl9DPe+NbmGvwg=", "cpu": [ "riscv64" ], @@ -328,7 +328,7 @@ }, "node_modules/@esbuild/linux-s390x": { "version": "0.28.1", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "integrity": "sha1-rOoDVtoODrwI+Xz3ucLkAeHmSNw=", "cpu": [ "s390x" ], @@ -344,7 +344,7 @@ }, "node_modules/@esbuild/linux-x64": { "version": "0.28.1", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "integrity": "sha1-bww84MtkxTS3DExF7LLBbTTjXf0=", "cpu": [ "x64" ], @@ -360,7 +360,7 @@ }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.28.1", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "integrity": "sha1-i813B3oNzjN4tXT+2ybSolO3PTY=", "cpu": [ "arm64" ], @@ -376,7 +376,7 @@ }, "node_modules/@esbuild/netbsd-x64": { "version": "0.28.1", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "integrity": "sha1-5/sqAemcgwyU5mI82f77TI+1g0c=", "cpu": [ "x64" ], @@ -392,7 +392,7 @@ }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.28.1", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "integrity": "sha1-xSkJNy24uG4sVeBaiUADO1Zgo7I=", "cpu": [ "arm64" ], @@ -408,7 +408,7 @@ }, "node_modules/@esbuild/openbsd-x64": { "version": "0.28.1", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "integrity": "sha1-xCe5vlpkwmL/mn63C1+7qt9EbGw=", "cpu": [ "x64" ], @@ -424,7 +424,7 @@ }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.28.1", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "integrity": "sha1-3JsUe6yi5sSzyFVxdB70hgpIkJc=", "cpu": [ "arm64" ], @@ -440,7 +440,7 @@ }, "node_modules/@esbuild/sunos-x64": { "version": "0.28.1", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "integrity": "sha1-zoZtEt8TwV5MmfBzo9Rm9uBkmzo=", "cpu": [ "x64" ], @@ -456,7 +456,7 @@ }, "node_modules/@esbuild/win32-arm64": { "version": "0.28.1", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "integrity": "sha1-dGjjaS0B1inVlB5dg4F7uA+eObQ=", "cpu": [ "arm64" ], @@ -472,7 +472,7 @@ }, "node_modules/@esbuild/win32-ia32": { "version": "0.28.1", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "integrity": "sha1-pbwAY/sryrbQ7WPyoVN5WLwmnsY=", "cpu": [ "ia32" ], @@ -488,7 +488,7 @@ }, "node_modules/@esbuild/win32-x64": { "version": "0.28.1", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "integrity": "sha1-EAZO5E9DR7kMmgK0Rrv4CpFjKxI=", "cpu": [ "x64" ], @@ -657,147 +657,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@github/copilot": { - "version": "1.0.83-3", - "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "detect-libc": "^2.1.2" - }, - "bin": { - "copilot": "npm-loader.js" - }, - "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-3", - "@github/copilot-darwin-x64": "1.0.83-3", - "@github/copilot-linux-arm64": "1.0.83-3", - "@github/copilot-linux-x64": "1.0.83-3", - "@github/copilot-linuxmusl-arm64": "1.0.83-3", - "@github/copilot-linuxmusl-x64": "1.0.83-3", - "@github/copilot-win32-arm64": "1.0.83-3", - "@github/copilot-win32-x64": "1.0.83-3" - } - }, - "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-3", - "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-arm64": "copilot" - } - }, - "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-3", - "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-x64": "copilot" - } - }, - "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-3", - "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-3", - "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-x64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-3", - "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-3", - "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-x64": "copilot" - } - }, - "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-3", - "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-arm64": "copilot.exe" - } - }, - "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-3", - "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-x64": "copilot.exe" - } - }, "node_modules/@glideapps/ts-necessities": { "version": "2.2.3", "integrity": "sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==", @@ -852,6 +711,18 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "integrity": "sha1-LVmuOrSzj7QnC/oj0w+OLobH/jI=", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", @@ -881,7 +752,7 @@ }, "node_modules/@koromix/koffi-darwin-x64": { "version": "3.1.0", - "integrity": "sha512-n/tVRB9xIzdXT5H3zZt8ueThgWTSDL+yU7PWnU8wbZPBSawP/otx3swQyd6nMOqj1bmHgSHopiKSBXRS9pllmg==", + "integrity": "sha1-r1z9mNiPAynDUcq8q6sPqKFY+xs=", "cpu": [ "x64" ], @@ -896,7 +767,7 @@ }, "node_modules/@koromix/koffi-freebsd-arm64": { "version": "3.1.0", - "integrity": "sha512-vazoPYIhOAlXZksVIqDRMIID4VeUZKx8F3dR90hOobT2ATyOkqNS5dv5UCV7Q7DSq22lQTrdbvENBAhROzCp0w==", + "integrity": "sha1-rOu888/eXKu5jj4KgZb0gCzt+xE=", "cpu": [ "arm64" ], @@ -911,7 +782,7 @@ }, "node_modules/@koromix/koffi-freebsd-ia32": { "version": "3.1.0", - "integrity": "sha512-Vm7Uc97ru6RTSVmae2zCZZQeaizqVZ8WoU4+gG4H03Qe+WOj7kbKt/MxT7VBzdbPYIU5ZJeG/ZED1YlZyab6eQ==", + "integrity": "sha1-0mky/r//V0JaXd+q9IRmdsZrkPI=", "cpu": [ "ia32" ], @@ -926,7 +797,7 @@ }, "node_modules/@koromix/koffi-freebsd-x64": { "version": "3.1.0", - "integrity": "sha512-N+VuVWjoiYPy1Go5mRadZ3B6RM5Qz+eCLhj2LXrMlefbUJ+O4gg7teCUGvPGfBEHDgmSN4yYUrfQmdJC10vOYw==", + "integrity": "sha1-2INR65Jz3bqyIPXHyqZ+PRGVkCs=", "cpu": [ "x64" ], @@ -941,7 +812,7 @@ }, "node_modules/@koromix/koffi-linux-arm64": { "version": "3.1.0", - "integrity": "sha512-Wx5iOkeALe2ympLdiYwRpIg5qUkyQIv8N2foZ9rRker0uE7ZtXew2RRkbEgMir4b0yDYR1zyXd6B62GUzLtZ/g==", + "integrity": "sha1-9h8pjdDbtKufG4JXoxDFRNzAbnI=", "cpu": [ "arm64" ], @@ -956,7 +827,7 @@ }, "node_modules/@koromix/koffi-linux-ia32": { "version": "3.1.0", - "integrity": "sha512-1DjYm1QehXU0dgn0uE+FGYOb3Of7GiTMqLS+ZI2gbl1b+h76sz4LRBvDVrQyAmSMVVU8/7696S21YgE/iBhBVg==", + "integrity": "sha1-8AbgHpYQoyx0uSoDgGpHwc1sGvM=", "cpu": [ "ia32" ], @@ -971,7 +842,7 @@ }, "node_modules/@koromix/koffi-linux-loong64": { "version": "3.1.0", - "integrity": "sha512-NOa0LdyltdESz3oeTqUH6MErHVoJOHoeXIsEp6xIMTUh4eKXEtlDQeoK6EYqo0DnBt83Xud95qLvi4Aw12pG4Q==", + "integrity": "sha1-bQvI/dvGGdARiMeDkZg0R+POkPI=", "cpu": [ "loong64" ], @@ -986,7 +857,7 @@ }, "node_modules/@koromix/koffi-linux-riscv64": { "version": "3.1.0", - "integrity": "sha512-Ye6kiXZCGxGtAIXSly6XuOP5tJZNYOZ2eVg33k1MilKrzimAy9Mpw4d6e9+Sfsc1jesgeNYs1sb5iaI8HS3ncA==", + "integrity": "sha1-V5aXxWe4DH2j2r1dnJcMH9w44co=", "cpu": [ "riscv64" ], @@ -1001,7 +872,7 @@ }, "node_modules/@koromix/koffi-linux-x64": { "version": "3.1.0", - "integrity": "sha512-3yQTOkQrMna4VX+yeyfYImBjLlGrItMpsWyfaW1uSiz/A6GRydqdwYH7DWnp4Z+RSGYZpsewkf7byMc8pOOQKA==", + "integrity": "sha1-eJX/wAVe0HJASLRTgubjJrHppuo=", "cpu": [ "x64" ], @@ -1016,7 +887,7 @@ }, "node_modules/@koromix/koffi-openbsd-ia32": { "version": "3.1.0", - "integrity": "sha512-/cDoFHb9yx4+yoT3GUpnKnfi3W2drG+/Ewo0TTZaQHb4PsxnYYyT6V8+t4cL5XXbQcTTcOsZxpmBRrn0NBa3dA==", + "integrity": "sha1-LwhKcVohSKoVCgY945O/4/gyoSk=", "cpu": [ "ia32" ], @@ -1031,7 +902,7 @@ }, "node_modules/@koromix/koffi-openbsd-x64": { "version": "3.1.0", - "integrity": "sha512-CoQdqgnKvWgTXXZlUst8cBRQEov7QsxlTN2WAsu9wez01Xe6gEcH/zYePANualzzCbnaELfe5P0rA80QkoDuPA==", + "integrity": "sha1-wWm472ijVY45okrdH/8EIAVtzAo=", "cpu": [ "x64" ], @@ -1046,7 +917,7 @@ }, "node_modules/@koromix/koffi-win32-ia32": { "version": "3.1.0", - "integrity": "sha512-WjrA+DEkpy0xEHu48+NSOboHhTnzkIfsFuq3d/WrSs+T9WflWRng3jC7mdJxmR4eHb6i6BqjW3k/U0mNUTjFPA==", + "integrity": "sha1-nDF+wS4sK934tD6A6e4I036PANM=", "cpu": [ "ia32" ], @@ -1061,7 +932,7 @@ }, "node_modules/@koromix/koffi-win32-x64": { "version": "3.1.0", - "integrity": "sha512-tnK5+IkzQBauQAQSzuyjso8OOIQRlaTZS39xIWpfqVYDLVDIuLDQk/WwHcOrR5yxlDrZq9ygiebBTOfcJFia7w==", + "integrity": "sha1-FyJ7F9SaNAIYddhe6gJ533eKOI4=", "cpu": [ "x64" ], @@ -1075,21 +946,24 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.2.3", + "integrity": "sha1-l+PUXXQk3F2h1OMvO/OykvbBtEw=", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@oxc-project/types": { @@ -1112,7 +986,7 @@ }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "integrity": "sha1-VM6Pg4IhP0oxSgwve6g/gf/q5ZI=", "cpu": [ "arm64" ], @@ -1144,7 +1018,7 @@ }, "node_modules/@rolldown/binding-darwin-x64": { "version": "1.0.3", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "integrity": "sha1-U/V94fWZ7PHbE4I8/IjBj7gJVK0=", "cpu": [ "x64" ], @@ -1160,7 +1034,7 @@ }, "node_modules/@rolldown/binding-freebsd-x64": { "version": "1.0.3", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "integrity": "sha1-bz/dobeuqsnSaKUmgEtPuW5ONfE=", "cpu": [ "x64" ], @@ -1176,7 +1050,7 @@ }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { "version": "1.0.3", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "integrity": "sha1-2HpFS/WFzJZ2hJN36R1uN1KXMm8=", "cpu": [ "arm" ], @@ -1192,7 +1066,7 @@ }, "node_modules/@rolldown/binding-linux-arm64-gnu": { "version": "1.0.3", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "integrity": "sha1-QZ/Wv2Es80jxBSjLzZTrq5YH2NE=", "cpu": [ "arm64" ], @@ -1208,7 +1082,7 @@ }, "node_modules/@rolldown/binding-linux-arm64-musl": { "version": "1.0.3", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "integrity": "sha1-/MaRhpa7doRId+HkkwoY/Q03QGk=", "cpu": [ "arm64" ], @@ -1224,7 +1098,7 @@ }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { "version": "1.0.3", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "integrity": "sha1-Mq7LfI2uXU8qjN5XoFjshpkVQvg=", "cpu": [ "ppc64" ], @@ -1240,7 +1114,7 @@ }, "node_modules/@rolldown/binding-linux-s390x-gnu": { "version": "1.0.3", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "integrity": "sha1-vtk0bqgea7i5PPEfXYi3fbiQt2M=", "cpu": [ "s390x" ], @@ -1256,7 +1130,7 @@ }, "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.0.3", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "integrity": "sha1-ZMLSb3Xf/ZtaH5dVegCudyUMjLc=", "cpu": [ "x64" ], @@ -1272,7 +1146,7 @@ }, "node_modules/@rolldown/binding-linux-x64-musl": { "version": "1.0.3", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "integrity": "sha1-WkUTLopHZZ7qrztUDClUqXyGD/M=", "cpu": [ "x64" ], @@ -1288,7 +1162,7 @@ }, "node_modules/@rolldown/binding-openharmony-arm64": { "version": "1.0.3", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "integrity": "sha1-KQUTBoxV6EnchFejKv7h17Csswk=", "cpu": [ "arm64" ], @@ -1304,7 +1178,7 @@ }, "node_modules/@rolldown/binding-wasm32-wasi": { "version": "1.0.3", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "integrity": "sha1-PZly2/GpU9PHr6pKDyDvKy458xs=", "cpu": [ "wasm32" ], @@ -1322,7 +1196,7 @@ }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.3", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "integrity": "sha1-oASrYHoW1vA7y1VXKP+IivdXc60=", "cpu": [ "arm64" ], @@ -1338,7 +1212,7 @@ }, "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.0.3", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "integrity": "sha1-4qJbNGkaHMihIJ195wkGMCbdDNs=", "cpu": [ "x64" ], @@ -1365,8 +1239,8 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "integrity": "sha1-AVy6np3UfOFNA9KoxdVHv7FpZl0=", "dev": true, "license": "MIT", "optional": true, @@ -1933,6 +1807,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/chownr": { + "version": "3.0.0", + "integrity": "sha1-mFXmTs0kCpzEJnzopKpdJKHaFeQ=", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/collection-utils": { "version": "1.0.1", "integrity": "sha512-LA2YTIlR7biSpXkKYwwuzGjwL5rjWEZVOSnvdUc7gObvWe4WkjxOpfrdhoP7Hs09YWDVfg0Mal9BpAqLfVEzQg==", @@ -2018,6 +1901,7 @@ "node_modules/detect-libc": { "version": "2.1.2", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2695,7 +2579,7 @@ }, "node_modules/lightningcss-android-arm64": { "version": "1.32.0", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "integrity": "sha1-8DOIURbf79nG9UeHUj41FLYeGWg=", "cpu": [ "arm64" ], @@ -2735,7 +2619,7 @@ }, "node_modules/lightningcss-darwin-x64": { "version": "1.32.0", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "integrity": "sha1-NfPpczLRMLnKGB4RtWje1q68bV4=", "cpu": [ "x64" ], @@ -2755,7 +2639,7 @@ }, "node_modules/lightningcss-freebsd-x64": { "version": "1.32.0", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "integrity": "sha1-l3enZHK2Ttb/lDQq1kx7r9eUpXU=", "cpu": [ "x64" ], @@ -2775,7 +2659,7 @@ }, "node_modules/lightningcss-linux-arm-gnueabihf": { "version": "1.32.0", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "integrity": "sha1-E65lLhq3O5E117faFy9mbEEK1T0=", "cpu": [ "arm" ], @@ -2795,7 +2679,7 @@ }, "node_modules/lightningcss-linux-arm64-gnu": { "version": "1.32.0", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "integrity": "sha1-QXhYeVqUWS9oASOhsfnaig4e8zU=", "cpu": [ "arm64" ], @@ -2815,7 +2699,7 @@ }, "node_modules/lightningcss-linux-arm64-musl": { "version": "1.32.0", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "integrity": "sha1-a+NmkugQtxgECAL9gJYjz/5zITM=", "cpu": [ "arm64" ], @@ -2835,7 +2719,7 @@ }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "integrity": "sha1-C3gDr06yHP043Tn+Kru1PH3QkfY=", "cpu": [ "x64" ], @@ -2855,7 +2739,7 @@ }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.32.0", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "integrity": "sha1-iNyLqGXd3bGsXvBLDxYYBEGMFjs=", "cpu": [ "x64" ], @@ -2875,7 +2759,7 @@ }, "node_modules/lightningcss-win32-arm64-msvc": { "version": "1.32.0", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "integrity": "sha1-TzC6P6XpJfW3n5RejMDRdsOxqzg=", "cpu": [ "arm64" ], @@ -2895,7 +2779,7 @@ }, "node_modules/lightningcss-win32-x64-msvc": { "version": "1.32.0", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "integrity": "sha1-FBqlYFZFBkkokCu0rwRfp9n0Igo=", "cpu": [ "x64" ], @@ -3012,6 +2896,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "integrity": "sha1-atdsOo8QInybUdHJrI4wsn9aJRw=", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/ms": { "version": "2.1.3", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", @@ -3492,6 +3388,22 @@ "node": ">=8" } }, + "node_modules/tar": { + "version": "7.5.22", + "integrity": "sha1-ppb5mBNucUh9w/hpqFu6LGeXG6k=", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tiny-inflate": { "version": "1.0.3", "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", @@ -3558,7 +3470,7 @@ }, "node_modules/tslib": { "version": "2.8.1", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", "dev": true, "license": "0BSD", "optional": true @@ -3910,6 +3822,15 @@ } } }, + "node_modules/yallist": { + "version": "5.0.0", + "integrity": "sha1-AOLeRDY57Q14/YfeDSdGn7z/tTM=", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yaml": { "version": "2.9.0", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", diff --git a/nodejs/package.json b/nodejs/package.json index adbc639cb0..c937d83bf7 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -5,6 +5,7 @@ "url": "https://github.com/github/copilot-sdk.git" }, "version": "0.0.0-dev", + "copilotCliVersion": "1.0.83-3", "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", "main": "./dist/cjs/index.js", "types": "./dist/index.d.ts", @@ -34,6 +35,9 @@ "scripts": { "clean": "rimraf --glob dist *.tgz", "build": "tsx esbuild-copilotsdk-nodejs.ts", + "pack:release": "tsx scripts/package-sdk.ts", + "verify:release-packages": "tsx scripts/verify-release-packages.ts", + "prepare:runtime": "tsx scripts/prepare-runtime.ts", "test": "vitest run", "test:watch": "vitest", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" --ignore-path .prettierignore", @@ -42,9 +46,10 @@ "lint:fix": "eslint --fix \"src/**/*.ts\" \"test/**/*.ts\"", "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", "generate": "cd ../scripts/codegen && npm run generate", + "set:cli-version": "node scripts/set-cli-version.js", "update:protocol-version": "tsx scripts/update-protocol-version.ts", "prepublishOnly": "npm run build", - "package": "npm run clean && npm run build && node scripts/set-version.js && npm pack && npm version 0.0.0-dev --no-git-tag-version --allow-same-version" + "package": "npm run clean && npm run build && node scripts/set-version.js && npm run pack:release && npm version 0.0.0-dev --no-git-tag-version --allow-same-version" }, "keywords": [ "github", @@ -56,7 +61,6 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-3", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -76,6 +80,7 @@ "quicktype-core": "^23.2.6", "rimraf": "^6.1.2", "semver": "^7.7.3", + "tar": "^7.5.22", "tsx": "^4.20.6", "typescript": "^5.0.0", "vitest": "^4.0.18", diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 09df5b1ff1..e2a9ab8a00 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,6 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-3", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -38,6 +37,7 @@ "quicktype-core": "^23.2.6", "rimraf": "^6.1.2", "semver": "^7.7.3", + "tar": "^7.5.22", "tsx": "^4.20.6", "typescript": "^5.0.0", "vitest": "^4.0.18", @@ -47,10 +47,2535 @@ "node": "^20.19.0 || >=22.12.0" } }, + "../node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "../node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "../node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "../node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "../node_modules/@eslint/config-array": { + "version": "0.21.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../node_modules/@eslint/js": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "../node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@glideapps/ts-necessities": { + "version": "2.2.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "../node_modules/@humanfs/node": { + "version": "0.16.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "../node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "../node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "../node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/@koromix/koffi-darwin-arm64": { + "version": "3.1.0", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "../node_modules/@oxc-project/types": { + "version": "0.133.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "../node_modules/@platformatic/vfs": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 22" + } + }, + "../node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "../node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/@standard-schema/spec": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "../node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/node": { + "version": "25.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "../node_modules/@types/ws": { + "version": "8.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "../node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/@vitest/expect": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/mocker": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "../node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/runner": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/snapshot": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/spy": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/utils": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/abort-controller": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "../node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "../node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "../node_modules/ajv": { + "version": "6.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "../node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "../node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "../node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../node_modules/brace-expansion": { + "version": "1.1.16", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "../node_modules/browser-or-node": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/buffer": { + "version": "6.0.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "../node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../node_modules/chai": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "../node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "../node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../node_modules/chownr": { + "version": "3.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "../node_modules/collection-utils": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0" + }, + "../node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "../node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "../node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/cross-fetch": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "../node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "../node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "../node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "../node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "../node_modules/es-module-lexer": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/esbuild": { + "version": "0.28.1", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "../node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/eslint": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "../node_modules/eslint-scope": { + "version": "8.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../node_modules/espree": { + "version": "10.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/esquery": { + "version": "1.6.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "../node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "../node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "../node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "../node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/event-target-shim": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../node_modules/events": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "../node_modules/expect-type": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "../node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "../node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "../node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "../node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "../node_modules/flatted": { + "version": "3.4.2", + "dev": true, + "license": "ISC" + }, + "../node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "../node_modules/glob": { + "version": "13.0.6", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "../node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "../node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "../node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/is-url": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "../node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../node_modules/js-base64": { + "version": "3.7.8", + "dev": true, + "license": "BSD-3-Clause" + }, + "../node_modules/js-yaml": { + "version": "4.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "../node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/json-schema": { + "version": "0.4.0", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "../node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "../node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "../node_modules/koffi": { + "version": "3.1.0", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + }, + "optionalDependencies": { + "@koromix/koffi-darwin-arm64": "3.1.0", + "@koromix/koffi-darwin-x64": "3.1.0", + "@koromix/koffi-freebsd-arm64": "3.1.0", + "@koromix/koffi-freebsd-ia32": "3.1.0", + "@koromix/koffi-freebsd-x64": "3.1.0", + "@koromix/koffi-linux-arm64": "3.1.0", + "@koromix/koffi-linux-ia32": "3.1.0", + "@koromix/koffi-linux-loong64": "3.1.0", + "@koromix/koffi-linux-riscv64": "3.1.0", + "@koromix/koffi-linux-x64": "3.1.0", + "@koromix/koffi-openbsd-ia32": "3.1.0", + "@koromix/koffi-openbsd-x64": "3.1.0", + "@koromix/koffi-win32-ia32": "3.1.0", + "@koromix/koffi-win32-x64": "3.1.0" + } + }, + "../node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../node_modules/lightningcss": { + "version": "1.32.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "../node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "../node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/lodash": { + "version": "4.18.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/lru-cache": { + "version": "11.2.6", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "../node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "../node_modules/minimatch": { + "version": "10.2.4", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../node_modules/minimatch/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "../node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "../node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../node_modules/minipass": { + "version": "7.1.3", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "../node_modules/minizlib": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "../node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/nanoid": { + "version": "3.3.17", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "../node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/node-fetch": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "../node_modules/obug": { + "version": "2.1.1", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "../node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "../node_modules/pako": { + "version": "1.0.11", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "../node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "../node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../node_modules/path-scurry": { + "version": "2.0.2", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "../node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../node_modules/pluralize": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "../node_modules/postcss": { + "version": "8.5.25", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "../node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "../node_modules/prettier": { + "version": "3.8.1", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "../node_modules/process": { + "version": "0.11.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "../node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../node_modules/quicktype-core": { + "version": "23.2.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@glideapps/ts-necessities": "2.2.3", + "browser-or-node": "^3.0.0", + "collection-utils": "^1.0.1", + "cross-fetch": "^4.0.0", + "is-url": "^1.2.4", + "js-base64": "^3.7.7", + "lodash": "^4.17.21", + "pako": "^1.0.6", + "pluralize": "^8.0.0", + "readable-stream": "4.5.2", + "unicode-properties": "^1.4.1", + "urijs": "^1.19.1", + "wordwrap": "^1.0.0", + "yaml": "^2.4.1" + } + }, + "../node_modules/readable-stream": { + "version": "4.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "../node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "../node_modules/rimraf": { + "version": "6.1.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../node_modules/rolldown": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "../node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../node_modules/semver": { + "version": "7.7.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "../node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/std-env": { + "version": "4.1.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "../node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../node_modules/tar": { + "version": "7.5.22", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "../node_modules/tiny-inflate": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/tinyexec": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "../node_modules/tinyglobby": { + "version": "0.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "../node_modules/tinyrainbow": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../node_modules/tr46": { + "version": "0.0.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/ts-api-utils": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "../node_modules/tsx": { + "version": "4.22.4", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "../node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "../node_modules/undici-types": { + "version": "7.18.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/unicode-properties": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "../node_modules/unicode-trie": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "../node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "dev": true, + "license": "MIT" + }, + "../node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "../node_modules/urijs": { + "version": "1.19.11", + "dev": true, + "license": "MIT" + }, + "../node_modules/vite": { + "version": "8.0.16", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "../node_modules/vitest": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "../node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../node_modules/webidl-conversions": { + "version": "3.0.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "../node_modules/whatwg-url": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "../node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "../node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "../node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/wordwrap": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/ws": { + "version": "8.21.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "../node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "../node_modules/yaml": { + "version": "2.9.0", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "../node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/zod": { + "version": "4.3.6", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha1-v24QMDvPLnxoaXX6Uvk37Cco2Lw=", "cpu": [ "ppc64" ], @@ -65,9 +2590,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha1-LYTs5qTiaE2SvibuE9QnV9gxw4E=", "cpu": [ "arm" ], @@ -82,9 +2607,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha1-DGJGvI0sTRcqrC2z+xGQ1yvWVQQ=", "cpu": [ "arm64" ], @@ -99,9 +2624,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha1-/DjU1jWNjcHPU/CfdYn+Q262SAE=", "cpu": [ "x64" ], @@ -116,9 +2641,7 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "version": "0.28.2", "cpu": [ "arm64" ], @@ -133,9 +2656,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha1-UQFHwFWnlViNu+FP1rG4rQovMN4=", "cpu": [ "x64" ], @@ -150,9 +2673,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha1-CTuSAOzwsRW6Tl4kinSFycX4vV4=", "cpu": [ "arm64" ], @@ -167,9 +2690,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha1-C+Irbfkl0hPoQeqHEjr134Cw+vc=", "cpu": [ "x64" ], @@ -184,9 +2707,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha1-vrEq1yuE9y0oSIzBuO6ffrFB11M=", "cpu": [ "arm" ], @@ -201,9 +2724,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha1-G9vGUc2pupmVxT7ZxxzqplCUdi0=", "cpu": [ "arm64" ], @@ -218,9 +2741,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha1-uB+dVVKbRcIGpGoTghSxqmh5aWs=", "cpu": [ "ia32" ], @@ -235,9 +2758,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha1-WYZnJBoEyZt27W75QKxQA4xBn5g=", "cpu": [ "loong64" ], @@ -252,9 +2775,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha1-HFHrnOqQP1PZe1rzsYQdtw9Vlso=", "cpu": [ "mips64el" ], @@ -269,9 +2792,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha1-Y91h8XzrMagSJ/QT/qyKcbwsUfI=", "cpu": [ "ppc64" ], @@ -286,9 +2809,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha1-N2Owj95c8lqx+suOd1Lt/kX7/Cc=", "cpu": [ "riscv64" ], @@ -303,9 +2826,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha1-GhN/8pOoKQbrMXY4W9fo4OXPt8s=", "cpu": [ "s390x" ], @@ -320,9 +2843,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha1-Jos2IRwUbKVPj+EsV4qNbviXlIU=", "cpu": [ "x64" ], @@ -337,9 +2860,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha1-Ilca2VHWK7aszILY0frVyMGsC6E=", "cpu": [ "arm64" ], @@ -354,9 +2877,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha1-QvzFcpfrCgyj9fxHUpH0waP3wN4=", "cpu": [ "x64" ], @@ -371,9 +2894,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha1-nrMq8QSsPaz07coB9ZZmSqsMc+8=", "cpu": [ "arm64" ], @@ -388,9 +2911,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha1-/r7SQC1giCJekfIPtM4lIq0KTv0=", "cpu": [ "x64" ], @@ -405,9 +2928,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha1-hWQcPUZkKL+8zqXyHCaDZmP+9c4=", "cpu": [ "arm64" ], @@ -422,9 +2945,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha1-pzb52JYkgQRfxMPlT1R58iyHD7Q=", "cpu": [ "x64" ], @@ -439,9 +2962,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha1-7lq0D60YYgG2UqM/il6xSenkJTI=", "cpu": [ "arm64" ], @@ -456,9 +2979,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha1-xA0optmaEn2mcR8q/XSxHLY7Bqc=", "cpu": [ "ia32" ], @@ -473,9 +2996,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha1-shr/uATMFnwTPZX0WzodwTI7moc=", "cpu": [ "x64" ], @@ -494,9 +3017,7 @@ "link": true }, "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "version": "22.20.1", "dev": true, "license": "MIT", "dependencies": { @@ -504,9 +3025,7 @@ } }, "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "version": "0.28.2", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -517,40 +3036,37 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -561,9 +3077,7 @@ } }, "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "version": "4.23.12", "dev": true, "license": "MIT", "dependencies": { @@ -581,8 +3095,6 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" } diff --git a/nodejs/scripts/package-sdk.ts b/nodejs/scripts/package-sdk.ts new file mode 100644 index 0000000000..fbdd7c6092 --- /dev/null +++ b/nodejs/scripts/package-sdk.ts @@ -0,0 +1,73 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { COPILOT_CLI_VERSION } from "../src/cliVersion.js"; +import { + getRuntimePackageName, + materializeRuntimeBundle, + RUNTIME_PLATFORMS, +} from "../src/runtimeArtifacts.js"; +import { ensureCopilotPackage } from "./releaseArtifacts.js"; + +const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const packagePath = join(nodeRoot, "package.json"); +const originalPackage = readFileSync(packagePath, "utf8"); +const packageJson = JSON.parse(originalPackage); +const sdkVersion = packageJson.version; +const npmCliPath = process.env.npm_execpath; +if (!npmCliPath) { + throw new Error("package-sdk.ts must be run through an npm script"); +} +const requestedPlatforms = process.env.COPILOT_SDK_RUNTIME_PLATFORMS?.split(",").filter(Boolean); +const platforms = requestedPlatforms ?? [...RUNTIME_PLATFORMS]; +const stagingRoot = mkdtempSync(join(tmpdir(), "copilot-sdk-platform-packages-")); + +try { + const optionalDependencies: Record = {}; + for (const platform of platforms) { + if (!(RUNTIME_PLATFORMS as readonly string[]).includes(platform)) { + throw new Error(`Unsupported runtime platform: ${platform}`); + } + const releasePackage = await ensureCopilotPackage(COPILOT_CLI_VERSION, { platform }); + const runtimeWrapper = materializeRuntimeBundle( + { packageRoot: releasePackage, platform }, + stagingRoot, + platform + ); + const runtimeRoot = resolve(dirname(runtimeWrapper), "..", ".."); + const packageName = getRuntimePackageName(platform); + const [osName, cpu] = platform.replace("linuxmusl", "linux").split("-"); + const runtimePackage = { + name: packageName, + version: sdkVersion, + description: `Platform runtime for @github/copilot-sdk (${platform})`, + license: "MIT", + os: [osName], + cpu: [cpu], + ...(platform.startsWith("linux") + ? { libc: [platform.startsWith("linuxmusl") ? "musl" : "glibc"] } + : {}), + }; + writeFileSync( + join(runtimeRoot, "package.json"), + `${JSON.stringify(runtimePackage, null, 4)}\n` + ); + execFileSync(process.execPath, [npmCliPath, "pack", runtimeRoot, "--pack-destination", nodeRoot], { + stdio: "inherit", + }); + optionalDependencies[packageName] = sdkVersion; + } + + packageJson.optionalDependencies = optionalDependencies; + writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 4)}\n`); + execFileSync( + process.execPath, + [npmCliPath, "pack", nodeRoot, "--pack-destination", nodeRoot], + { stdio: "inherit" } + ); +} finally { + writeFileSync(packagePath, originalPackage); + rmSync(stagingRoot, { recursive: true, force: true }); +} diff --git a/nodejs/scripts/prepare-runtime.ts b/nodejs/scripts/prepare-runtime.ts new file mode 100644 index 0000000000..1cdfc0a6e7 --- /dev/null +++ b/nodejs/scripts/prepare-runtime.ts @@ -0,0 +1,16 @@ +import { join } from "node:path"; +import { getRuntimePlatform, materializeRuntimeBundle } from "../src/runtimeArtifacts.js"; +import { COPILOT_CLI_VERSION } from "../src/cliVersion.js"; +import { ensureCopilotPackage } from "./releaseArtifacts.js"; + +const [option] = process.argv.slice(2); +const platform = getRuntimePlatform(); +const packageRoot = await ensureCopilotPackage(COPILOT_CLI_VERSION, { platform }); +if (option === "--print-legacy-path") { + process.stdout.write(`${join(packageRoot, "app.js")}\n`); +} else if (option === "--print-path" || option === undefined) { + const runtimePath = materializeRuntimeBundle({ packageRoot, platform }); + process.stdout.write(`${runtimePath}\n`); +} else { + throw new Error(`Unknown option: ${option}`); +} diff --git a/nodejs/scripts/releaseArtifacts.ts b/nodejs/scripts/releaseArtifacts.ts new file mode 100644 index 0000000000..2731d878c5 --- /dev/null +++ b/nodejs/scripts/releaseArtifacts.ts @@ -0,0 +1,233 @@ +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { x as extractTar } from "tar"; +import { + defaultRuntimeCacheRoot, + getRuntimePlatform, + getRuntimeReleaseAssetName, + resolvePackageRoot, + validateFile, +} from "../src/runtimeArtifacts.js"; +import { COPILOT_CLI_USE_NPM_PACKAGE, COPILOT_CLI_VERSION } from "../src/cliVersion.js"; + +export interface EnsureCopilotPackageOptions { + cacheRoot?: string; + environment?: NodeJS.ProcessEnv; + fetch?: typeof globalThis.fetch; + fetchTimeoutMs?: number; + platform?: string; +} + +const packageDownloads = new Map>(); +const checksumDownloads = new Map>>(); +const DEFAULT_FETCH_TIMEOUT_MS = 60_000; + +async function fetchWithRetry( + fetcher: typeof globalThis.fetch, + url: string, + readResponse: (response: Response) => Promise, + timeoutMs: number +): Promise { + let lastError: unknown; + for (let attempt = 0; attempt < 3; attempt++) { + try { + const response = await fetcher(url, { + signal: AbortSignal.timeout(timeoutMs), + }); + if (response.ok) { + return await readResponse(response); + } + await response.body?.cancel(); + lastError = new Error(`${response.status} ${response.statusText}`); + if ( + response.status >= 400 && + response.status < 500 && + response.status !== 408 && + response.status !== 429 + ) { + break; + } + } catch (error) { + lastError = error; + } + if (attempt < 2) { + await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000)); + } + } + throw new Error(`Failed to download ${url}: ${String(lastError)}`); +} + +async function getReleaseChecksum( + version: string, + assetName: string, + baseUrl: string, + fetcher: typeof globalThis.fetch, + fetchTimeoutMs: number +): Promise { + const key = `${baseUrl}\0${version}`; + let checksums = checksumDownloads.get(key); + if (!checksums) { + checksums = downloadReleaseChecksums(version, baseUrl, fetcher, fetchTimeoutMs); + checksumDownloads.set(key, checksums); + try { + return (await checksums).get(assetName); + } catch (error) { + checksumDownloads.delete(key); + throw error; + } + } + return (await checksums).get(assetName); +} + +async function downloadReleaseChecksums( + version: string, + baseUrl: string, + fetcher: typeof globalThis.fetch, + fetchTimeoutMs: number +): Promise> { + const contents = await fetchWithRetry( + fetcher, + `${baseUrl}/v${version}/SHA256SUMS.txt`, + (response) => response.text(), + fetchTimeoutMs + ); + const checksums = new Map(); + for (const line of contents.split(/\r?\n/)) { + const [hash, name] = line.trim().split(/\s+/, 2); + if (/^[a-fA-F0-9]{64}$/.test(hash) && name) { + checksums.set(name.replace(/^\*/, ""), hash.toLowerCase()); + } + } + return checksums; +} + +export async function ensureCopilotPackage( + version = COPILOT_CLI_VERSION, + options: EnsureCopilotPackageOptions = {} +): Promise { + const platform = options.platform ?? getRuntimePlatform(); + // lgtm[js/trivial-conditional] This generated constant is true for internal canary builds. + if (version === COPILOT_CLI_VERSION && COPILOT_CLI_USE_NPM_PACKAGE) { + const packageName = `@github/copilot-${platform}`; + const packageRoot = resolvePackageRoot(packageName); + if (!packageRoot) { + throw new Error(`Could not resolve ${packageName} for Copilot CLI ${version}.`); + } + validateFile( + join(packageRoot, "prebuilds", platform, "runtime.node"), + "Copilot runtime.node" + ); + return packageRoot; + } + + const cacheRoot = options.cacheRoot ?? defaultRuntimeCacheRoot(); + const cachedPackageRoot = join(cacheRoot, version, "packages", platform); + const cachedRuntimeNode = join(cachedPackageRoot, "prebuilds", platform, "runtime.node"); + if (existsSync(cachedRuntimeNode)) { + validateFile(cachedRuntimeNode, "Copilot runtime.node"); + return cachedPackageRoot; + } + + const baseUrl = ( + (options.environment ?? process.env).COPILOT_CLI_DOWNLOAD_BASE_URL ?? + "https://github.com/github/copilot-cli/releases/download" + ).replace(/\/+$/, ""); + const fetchTimeoutMs = options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS; + const key = `${cacheRoot}\0${version}\0${platform}\0${baseUrl}`; + if (!options.fetch) { + const existing = packageDownloads.get(key); + if (existing) { + return existing; + } + const download = downloadCopilotPackage( + version, + platform, + cacheRoot, + baseUrl, + globalThis.fetch, + fetchTimeoutMs + ); + packageDownloads.set(key, download); + try { + return await download; + } finally { + packageDownloads.delete(key); + } + } + return downloadCopilotPackage( + version, + platform, + cacheRoot, + baseUrl, + options.fetch, + fetchTimeoutMs + ); +} + +async function downloadCopilotPackage( + version: string, + platform: string, + cacheRoot: string, + baseUrl: string, + fetcher: typeof globalThis.fetch, + fetchTimeoutMs: number +): Promise { + if (!fetcher) { + throw new Error("This Node.js runtime does not provide fetch()."); + } + const assetName = getRuntimeReleaseAssetName(version, platform); + const expectedChecksum = await getReleaseChecksum( + version, + assetName, + baseUrl, + fetcher, + fetchTimeoutMs + ); + if (!expectedChecksum) { + throw new Error(`SHA256SUMS.txt does not contain ${assetName}.`); + } + const archive = await fetchWithRetry( + fetcher, + `${baseUrl}/v${version}/${assetName}`, + async (response) => Buffer.from(await response.arrayBuffer()), + fetchTimeoutMs + ); + const actualChecksum = createHash("sha256").update(archive).digest("hex"); + if (actualChecksum !== expectedChecksum) { + throw new Error( + `Checksum mismatch for ${assetName}: expected ${expectedChecksum}, got ${actualChecksum}.` + ); + } + + mkdirSync(cacheRoot, { recursive: true }); + const stagingRoot = mkdtempSync(join(cacheRoot, ".download-")); + const archivePath = join(stagingRoot, assetName); + const packageRoot = join(stagingRoot, "package"); + const cachedPackageRoot = join(cacheRoot, version, "packages", platform); + writeFileSync(archivePath, archive); + try { + await extractTar({ + cwd: stagingRoot, + file: archivePath, + gzip: true, + preservePaths: false, + strict: true, + }); + validateFile( + join(packageRoot, "prebuilds", platform, "runtime.node"), + "Copilot runtime.node" + ); + mkdirSync(dirname(cachedPackageRoot), { recursive: true }); + try { + renameSync(packageRoot, cachedPackageRoot); + } catch (error) { + if (!existsSync(cachedPackageRoot)) { + throw error; + } + } + return cachedPackageRoot; + } finally { + rmSync(stagingRoot, { recursive: true, force: true }); + } +} diff --git a/nodejs/scripts/set-cli-version.js b/nodejs/scripts/set-cli-version.js new file mode 100644 index 0000000000..ea45f90ada --- /dev/null +++ b/nodejs/scripts/set-cli-version.js @@ -0,0 +1,71 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const [version, mode] = process.argv.slice(2); +if (!version || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z._-]+)?$/.test(version)) { + throw new Error("Usage: set-cli-version.js [--npm-package]"); +} +if (mode !== undefined && mode !== "--npm-package") { + throw new Error(`Unknown option: ${mode}`); +} + +const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const runtimePlatforms = [ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "linuxmusl-arm64", + "linuxmusl-x64", + "win32-arm64", + "win32-x64", +]; +const cliAssets = [ + "copilot-darwin-arm64.tar.gz", + "copilot-darwin-x64.tar.gz", + "copilot-linux-arm64.tar.gz", + "copilot-linux-x64.tar.gz", + "copilot-win32-arm64.zip", + "copilot-win32-x64.zip", +]; +const useNpmPackage = mode === "--npm-package"; +if (!useNpmPackage) { + const checksumsUrl = `https://github.com/github/copilot-cli/releases/download/v${version}/SHA256SUMS.txt`; + const response = await fetch(checksumsUrl); + if (!response.ok) { + throw new Error( + `Failed to download ${checksumsUrl}: ${response.status} ${response.statusText}` + ); + } + const checksums = new Map( + (await response.text()) + .split(/\r?\n/) + .map((line) => line.trim().split(/\s+/, 2)) + .filter(([hash, name]) => /^[a-fA-F0-9]{64}$/.test(hash) && name) + .map(([hash, name]) => [name.replace(/^\*/, ""), hash.toLowerCase()]) + ); + for (const assetName of [ + ...runtimePlatforms.map((platform) => `github-copilot-${version}-${platform}.tgz`), + ...cliAssets, + ]) { + if (!checksums.has(assetName)) { + throw new Error(`SHA256SUMS.txt does not contain ${assetName}`); + } + } +} +const packagePath = join(nodeRoot, "package.json"); +const packageJson = JSON.parse(readFileSync(packagePath, "utf8")); +packageJson.copilotCliVersion = version; +writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 4)}\n`); + +const sourcePath = join(nodeRoot, "src", "cliVersion.ts"); +writeFileSync( + sourcePath, + [ + `export const COPILOT_CLI_VERSION = ${JSON.stringify(version)};`, + "", + `export const COPILOT_CLI_USE_NPM_PACKAGE = ${useNpmPackage};`, + "", + ].join("\n") +); diff --git a/nodejs/scripts/verify-release-packages.ts b/nodejs/scripts/verify-release-packages.ts new file mode 100644 index 0000000000..e1b13a3074 --- /dev/null +++ b/nodejs/scripts/verify-release-packages.ts @@ -0,0 +1,100 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { globSync } from "glob"; +import { t as listTar, x as extractTar } from "tar"; +import { getRuntimePackageName, RUNTIME_PLATFORMS } from "../src/runtimeArtifacts.js"; + +interface PackedPackage { + manifest: { + name: string; + version: string; + optionalDependencies?: Record; + }; + entries: Set; +} + +const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const sourceManifest = JSON.parse( + readFileSync(join(nodeRoot, "package.json"), "utf8") +) as PackedPackage["manifest"]; +const expectedRuntimePackages = Object.fromEntries( + RUNTIME_PLATFORMS.map((platform) => [getRuntimePackageName(platform), sourceManifest.version]) +); +const expectedPackageNames = new Set([ + sourceManifest.name, + ...Object.keys(expectedRuntimePackages), +]); +const packages = new Map(); + +for (const archive of globSync("*.tgz", { cwd: nodeRoot, absolute: true })) { + const entries = new Set(); + await listTar({ + file: archive, + onReadEntry(entry) { + entries.add(entry.path); + entry.resume(); + }, + }); + + const manifestRoot = mkdtempSync(join(tmpdir(), "copilot-sdk-package-manifest-")); + let manifest: PackedPackage["manifest"]; + try { + await extractTar({ + cwd: manifestRoot, + file: archive, + strict: true, + filter: (entryPath) => entryPath === "package/package.json", + }); + manifest = JSON.parse( + readFileSync(join(manifestRoot, "package", "package.json"), "utf8") + ) as PackedPackage["manifest"]; + } finally { + rmSync(manifestRoot, { recursive: true, force: true }); + } + + if (manifest.version !== sourceManifest.version || !expectedPackageNames.has(manifest.name)) { + continue; + } + assert(!packages.has(manifest.name), `Duplicate tarball for ${manifest.name}`); + packages.set(manifest.name, { manifest, entries }); +} + +assert.deepEqual( + [...packages.keys()].sort(), + [...expectedPackageNames].sort(), + "Release packaging did not produce the expected main and platform packages" +); + +const mainPackage = packages.get(sourceManifest.name); +assert(mainPackage, `Missing ${sourceManifest.name} tarball`); +assert.deepEqual( + mainPackage.manifest.optionalDependencies, + expectedRuntimePackages, + "Main package optional dependencies do not match the platform packages" +); +assert(mainPackage.entries.has("package/dist/index.js"), "Main package is missing dist/index.js"); +assert( + mainPackage.entries.has("package/dist/cjs/index.js"), + "Main package is missing dist/cjs/index.js" +); + +for (const platform of RUNTIME_PLATFORMS) { + const packageName = getRuntimePackageName(platform); + const packed = packages.get(packageName); + assert(packed, `Missing ${packageName} tarball`); + const runtimeName = platform.startsWith("win32") ? "copilot-runtime.exe" : "copilot-runtime"; + for (const requiredPath of [ + `package/prebuilds/${platform}/${runtimeName}`, + `package/prebuilds/${platform}/runtime.node`, + "package/copilot-sdk/extension.js", + "package/preloads/extension_bootstrap.mjs", + "package/sdk/index.js", + ]) { + assert(packed.entries.has(requiredPath), `${packageName} is missing ${requiredPath}`); + } +} + +console.log(`Verified ${packages.size} release package tarballs.`); diff --git a/nodejs/src/cliVersion.ts b/nodejs/src/cliVersion.ts new file mode 100644 index 0000000000..a0c4eb8558 --- /dev/null +++ b/nodejs/src/cliVersion.ts @@ -0,0 +1,3 @@ +export const COPILOT_CLI_VERSION = "1.0.83-3"; + +export const COPILOT_CLI_USE_NPM_PACKAGE = false; diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 83b0a5f483..51c3e6650d 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -14,10 +14,8 @@ import { spawn, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; -import { createRequire } from "node:module"; import { Socket } from "node:net"; import { dirname, isAbsolute, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; import { createMessageConnection, ErrorCodes, @@ -44,7 +42,8 @@ import type { import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession } from "./session.js"; import type { FfiRuntimeHost } from "./ffiRuntimeHost.js"; -import { materializeRuntimeBundle } from "./runtimeArtifacts.js"; +import { ensureRuntimeBundle } from "./runtimeArtifacts.js"; +import { COPILOT_CLI_VERSION } from "./cliVersion.js"; import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js"; import { createCopilotRequestAdapter } from "./copilotRequestHandler.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; @@ -371,83 +370,8 @@ function getNodeExecPath(): string { return process.execPath; } -/** - * Computes the candidate platform-specific CLI package names for the current - * platform/arch, mirroring @github/copilot's npm-loader. As of CLI 1.0.64-1 the - * @github/copilot package is a thin loader and the actual CLI ships in a - * platform package (e.g. @github/copilot-darwin-arm64). For Linux we try both - * the glibc and musl variants since only the matching one is installed. - */ -function getCliPlatformPackageNames(): string[] { - const arch = process.arch; - const variants = process.platform === "linux" ? ["linux", "linuxmusl"] : [process.platform]; - return variants.map((variant) => `@github/copilot-${variant}-${arch}`); -} - -interface BundledCliPackage { - root: string; - platform: string; -} - -/** - * Resolves the current platform package and its npm prebuilds folder. - * - * In ESM, uses import.meta.resolve directly. In CJS (e.g., VS Code extensions - * bundled with esbuild format:"cjs"), import.meta is empty so we fall back to - * walking node_modules to find the package. - */ -function getBundledCliPackage(): BundledCliPackage { - const packageNames = getCliPlatformPackageNames(); - - if (typeof import.meta.resolve === "function") { - // ESM: resolve via import.meta.resolve - for (const packageName of packageNames) { - try { - const packageEntryUrl = import.meta.resolve(packageName); - const packageEntryPath = fileURLToPath(packageEntryUrl); - return { - root: dirname(packageEntryPath), - platform: packageName.slice("@github/copilot-".length), - }; - } catch { - // Try the next candidate platform package. - } - } - throw new Error( - `Could not resolve a @github/copilot platform package (tried ${packageNames.join(", ")}). ` + - `Ensure @github/copilot is installed, or pass cliPath/cliUrl to CopilotClient.` - ); - } - - // CJS fallback: the platform packages have ESM-only exports so - // require.resolve cannot reach them. Walk the module search paths instead. - const req = createRequire(__filename); - const searchPaths = req.resolve.paths("@github/copilot") ?? []; - for (const base of searchPaths) { - for (const packageName of packageNames) { - const root = join(base, ...packageName.split("/")); - const candidate = join(root, "index.js"); - if (existsSync(candidate)) { - return { - root, - platform: packageName.slice("@github/copilot-".length), - }; - } - } - } - throw new Error( - `Could not find a @github/copilot platform package (tried ${packageNames.join(", ")}). ` + - `Searched ${searchPaths.length} paths. ` + - `Ensure @github/copilot is installed, or pass cliPath/cliUrl to CopilotClient.` - ); -} - -function getBundledRuntimePath(): string { - const bundled = getBundledCliPackage(); - return materializeRuntimeBundle({ - packageRoot: bundled.root, - platform: bundled.platform, - }); +function getBundledRuntimePath(): Promise { + return ensureRuntimeBundle(COPILOT_CLI_VERSION); } /** @@ -776,8 +700,6 @@ export class CopilotClient { const explicitCliPath = conn.path ?? effectiveEnv.COPILOT_CLI_PATH; if (explicitCliPath) { this.resolvedCliPath = explicitCliPath; - } else { - this.resolvedCliPath = getBundledRuntimePath(); } } @@ -2639,6 +2561,7 @@ export class CopilotClient { * Start the CLI server process */ private async startCLIServer(): Promise { + this.resolvedCliPath ??= await getBundledRuntimePath(); return new Promise((resolve, reject) => { // Clear stderr buffer for fresh capture this.stderrBuffer = ""; @@ -2698,7 +2621,7 @@ export class CopilotClient { // Verify CLI exists before attempting to spawn if (!existsSync(this.resolvedCliPath)) { throw new Error( - `Copilot CLI not found at ${this.resolvedCliPath}. Ensure @github/copilot is installed.` + `Copilot CLI not found at ${this.resolvedCliPath}. Set COPILOT_CLI_PATH to use a custom installation.` ); } @@ -2842,14 +2765,21 @@ export class CopilotClient { /** Starts the in-process FFI runtime with SDK-managed typed options. */ private async startInProcessFfi(): Promise { const explicitEntrypoint = this.resolvedEnv.COPILOT_CLI_PATH; - const runtimeLibrary = explicitEntrypoint - ? join( - dirname(resolve(explicitEntrypoint)), - "prebuilds", - CopilotClient.getNapiPrebuildsFolder(explicitEntrypoint), - "runtime.node" - ) - : join(dirname(getBundledRuntimePath()), "runtime.node"); + let runtimeLibrary: string; + if (explicitEntrypoint) { + const entrypointDirectory = dirname(resolve(explicitEntrypoint)); + const adjacentRuntime = join(entrypointDirectory, "runtime.node"); + runtimeLibrary = existsSync(adjacentRuntime) + ? adjacentRuntime + : join( + entrypointDirectory, + "prebuilds", + CopilotClient.getNapiPrebuildsFolder(explicitEntrypoint), + "runtime.node" + ); + } else { + runtimeLibrary = join(dirname(await getBundledRuntimePath()), "runtime.node"); + } // Load the FFI host lazily so the native `koffi` addon (and its // platform-specific `koffi.node`) is only loaded on the in-process path; // out-of-process (stdio/tcp) consumers never touch the native dependency. diff --git a/nodejs/src/runtimeArtifacts.ts b/nodejs/src/runtimeArtifacts.ts index 19d9926250..d33b72c9da 100644 --- a/nodejs/src/runtimeArtifacts.ts +++ b/nodejs/src/runtimeArtifacts.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import { chmodSync, copyFileSync, @@ -11,32 +10,50 @@ import { rmSync, statSync, } from "node:fs"; +import { createHash } from "node:crypto"; +import { createRequire } from "node:module"; import { homedir } from "node:os"; import { dirname, join, relative, sep } from "node:path"; +import { COPILOT_CLI_USE_NPM_PACKAGE } from "./cliVersion.js"; export interface RuntimeArtifactSources { packageRoot: string; platform: string; } +export interface EnsureRuntimeBundleOptions { + cacheRoot?: string; + packageSearchPaths?: string[]; + platform?: string; +} + +const require = createRequire(typeof __filename === "string" ? __filename : import.meta.url); +export const RUNTIME_PLATFORMS = [ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "linuxmusl-arm64", + "linuxmusl-x64", + "win32-arm64", + "win32-x64", +] as const; + const EXCLUDED_TOP_LEVEL = new Set([ "app.js", "assets", "changelog.json", "copilot", "copilot.exe", - "copilot-sdk", "foundry-local-sdk", "index.js", "LICENSE.md", "napi-oop-runtime", "npm-loader.js", "package.json", - "preloads", "pvrecorder", "queries", "README.md", - "sdk", "sea-loader.js", "webview", ]); @@ -46,7 +63,7 @@ interface RuntimeAsset { relativePath: string; } -function validateFile(path: string, label: string): void { +export function validateFile(path: string, label: string): void { if (!existsSync(path)) { throw new Error(`${label} not found at ${path}.`); } @@ -92,14 +109,12 @@ function collectRuntimeAssets(sources: RuntimeArtifactSources): RuntimeAsset[] { } const parts = sourceRelative.split(sep); - let relativePath = sourceRelative; if (parts[0] === "prebuilds") { if (parts[1] !== sources.platform || parts.length < 3) { continue; } - relativePath = parts.slice(2).join(sep); } - assets.push({ source, relativePath }); + assets.push({ source, relativePath: sourceRelative }); } }; visit(sources.packageRoot); @@ -142,17 +157,25 @@ export function defaultRuntimeCacheRoot( export function materializeRuntimeBundle( sources: RuntimeArtifactSources, - cacheRoot = defaultRuntimeCacheRoot() + cacheRoot = defaultRuntimeCacheRoot(), + cacheKey = `${sources.platform}-${sourceFingerprint(collectRuntimeAssets(sources))}` ): string { const assets = collectRuntimeAssets(sources); - const wrapperName = process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; - const sourceWrapper = assets.find((asset) => asset.relativePath === wrapperName)?.source; - const sourceRuntimeNode = assets.find((asset) => asset.relativePath === "runtime.node")?.source; + const wrapperName = sources.platform.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; + const prebuildDir = join("prebuilds", sources.platform); + const wrapperRelative = join(prebuildDir, wrapperName); + const runtimeNodeRelative = join(prebuildDir, "runtime.node"); + const sourceWrapper = assets.find((asset) => asset.relativePath === wrapperRelative)?.source; + const sourceRuntimeNode = assets.find( + (asset) => asset.relativePath === runtimeNodeRelative + )?.source; validateRuntimeBundle(sourceWrapper ?? "", sourceRuntimeNode ?? ""); - const installDir = join(cacheRoot, `${sources.platform}-${sourceFingerprint(assets)}`); - const installedWrapper = join(installDir, wrapperName); - const installedRuntimeNode = join(installDir, "runtime.node"); + const installDir = join(cacheRoot, cacheKey); + const installedWrapper = join(installDir, wrapperRelative); + const installedRuntimeNode = join(installDir, runtimeNodeRelative); if (existsSync(installDir)) { validateRuntimeBundle(installedWrapper, installedRuntimeNode); makeExecutable(installedWrapper); @@ -167,7 +190,7 @@ export function materializeRuntimeBundle( mkdirSync(dirname(destination), { recursive: true }); copyFileSync(asset.source, destination); } - const stagedWrapper = join(stagingDir, wrapperName); + const stagedWrapper = join(stagingDir, wrapperRelative); makeExecutable(stagedWrapper); renameSync(stagingDir, installDir); } catch (error) { @@ -181,3 +204,92 @@ export function materializeRuntimeBundle( return installedWrapper; } + +function isMusl(): boolean { + if (process.platform !== "linux") { + return false; + } + const report = process.report?.getReport() as + | { header?: { glibcVersionRuntime?: string } } + | undefined; + return report?.header?.glibcVersionRuntime === undefined; +} + +export function getRuntimePlatform( + platform = process.platform, + arch = process.arch, + musl = isMusl() +): string { + if (arch !== "x64" && arch !== "arm64") { + throw new Error(`Unsupported Copilot CLI architecture: ${arch}.`); + } + if (platform === "linux") { + return `${musl ? "linuxmusl" : "linux"}-${arch}`; + } + if (platform === "darwin" || platform === "win32") { + return `${platform}-${arch}`; + } + throw new Error(`Unsupported Copilot CLI platform: ${platform}-${arch}.`); +} + +export function getRuntimeReleaseAssetName(version: string, platform: string): string { + return `github-copilot-${version}-${platform}.tgz`; +} + +export function getRuntimePackageName(platform: string): string { + return `@github/copilot-sdk-${platform}`; +} + +export function resolvePackageRoot( + packageName: string, + searchPaths = require.resolve.paths(packageName) ?? [] +): string | undefined { + return searchPaths + .map((base) => join(base, ...packageName.split("/"))) + .find((candidate) => existsSync(join(candidate, "package.json"))); +} + +export async function ensureRuntimeBundle( + version: string, + options: EnsureRuntimeBundleOptions = {} +): Promise { + const platform = options.platform ?? getRuntimePlatform(); + // lgtm[js/trivial-conditional] This generated constant is true for internal canary builds. + if (COPILOT_CLI_USE_NPM_PACKAGE) { + const packageName = `@github/copilot-${platform}`; + const packageRoot = resolvePackageRoot(packageName, options.packageSearchPaths); + if (!packageRoot) { + throw new Error(`Could not resolve ${packageName} for Copilot CLI ${version}.`); + } + validateFile( + join(packageRoot, "prebuilds", platform, "runtime.node"), + "Copilot runtime.node" + ); + return materializeRuntimeBundle( + { packageRoot, platform }, + options.cacheRoot, + `${version}-${platform}` + ); + } + + const packageName = getRuntimePackageName(platform); + const packageRoot = resolvePackageRoot(packageName, options.packageSearchPaths); + if (!packageRoot) { + throw new Error( + `Could not resolve ${packageName}. Reinstall @github/copilot-sdk so its platform package is installed.` + ); + } + const wrapperName = platform.startsWith("win32") ? "copilot-runtime.exe" : "copilot-runtime"; + const prebuildDir = join(packageRoot, "prebuilds", platform); + const wrapper = join(prebuildDir, wrapperName); + try { + validateRuntimeBundle(wrapper, join(prebuildDir, "runtime.node")); + } catch (error) { + throw new Error( + `${packageName} is missing required Copilot CLI runtime files. Reinstall @github/copilot-sdk.`, + { cause: error } + ); + } + makeExecutable(wrapper); + return wrapper; +} diff --git a/nodejs/test/e2e/event_fidelity.e2e.test.ts b/nodejs/test/e2e/event_fidelity.e2e.test.ts index da4b8105ad..8ae2091b33 100644 --- a/nodejs/test/e2e/event_fidelity.e2e.test.ts +++ b/nodejs/test/e2e/event_fidelity.e2e.test.ts @@ -35,9 +35,9 @@ describe("Event Fidelity", async () => { const assistantIdx = types.lastIndexOf("assistant.message"); expect(userIdx).toBeLessThan(assistantIdx); - // session.idle should be last + // session.idle completes the conversational turn; metadata may follow it. const idleIdx = types.lastIndexOf("session.idle"); - expect(idleIdx).toBe(types.length - 1); + expect(assistantIdx).toBeLessThan(idleIdx); await session.disconnect(); }); diff --git a/nodejs/test/e2e/extension_env_access.e2e.test.ts b/nodejs/test/e2e/extension_env_access.e2e.test.ts index 38210a22fb..1aa4875dd4 100644 --- a/nodejs/test/e2e/extension_env_access.e2e.test.ts +++ b/nodejs/test/e2e/extension_env_access.e2e.test.ts @@ -39,7 +39,7 @@ interface ExtensionRun { * stdio, so a stub host observes exactly what the CLI observes. That is the only * way to cover this feature end to end today: the released CLI predates the host * half (github/copilot-agent-runtime#15144), so it ignores the request and grants - * nothing. Once the `@github/copilot` dependency carries the host half, the + * nothing. Once the pinned CLI release carries the host half, the * real-CLI case below can assert the grant instead. */ async function runExtensionAgainstStubHost(options: { @@ -188,7 +188,7 @@ const cliObservations = mkdtempSync(join(tmpdir(), "copilot-env-access-cli-")); const cliResultFile = join(cliObservations, "result"); const cliContext = await createSdkTestContext({ copilotClientOptions: { - connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + connection: RuntimeConnection.forStdio({ path: await getLegacyCliPathForTests() }), env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS", EXTENSION_ENV_REQUEST: "E2E_SDK_TOKEN", @@ -201,7 +201,7 @@ const cliContext = await createSdkTestContext({ // The released CLI ignores `requestedEnvironmentVariables`, so this covers the // half a real CLI can prove today: asking for variables does not break the join. -// It becomes the grant test once `@github/copilot` carries the host half. +// It becomes the grant test once the pinned CLI release carries the host half. it("joins a real CLI that does not support environment requests", async () => { const { workDir, copilotClient } = cliContext; const extensionDir = join(workDir, ".github", "extensions", "env-access"); diff --git a/nodejs/test/e2e/factory.e2e.test.ts b/nodejs/test/e2e/factory.e2e.test.ts index 2bf3ff17fb..98004406f9 100644 --- a/nodejs/test/e2e/factory.e2e.test.ts +++ b/nodejs/test/e2e/factory.e2e.test.ts @@ -15,7 +15,7 @@ import { retry } from "./harness/sdkTestHelper.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const factoryTestContext = await createSdkTestContext({ copilotClientOptions: { - connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + connection: RuntimeConnection.forStdio({ path: await getLegacyCliPathForTests() }), env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS,AGENT_FACTORIES", }, diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index 58c275c800..c4befb148e 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -12,6 +12,8 @@ import { afterAll, afterEach, beforeEach, onTestFailed, TestContext } from "vite import { CopilotClient, CopilotClientOptions, RuntimeConnection } from "../../../src"; import { CapiProxy } from "./CapiProxy"; import { formatError, retry } from "./sdkTestHelper"; +import { ensureCopilotPackage } from "../../../scripts/releaseArtifacts"; +import { COPILOT_CLI_VERSION } from "../../../src/cliVersion"; export const isCI = process.env.GITHUB_ACTIONS === "true"; export const DEFAULT_GITHUB_TOKEN = "fake-token-for-e2e-tests"; @@ -50,27 +52,10 @@ function getCliPathForTests(): string | undefined { return undefined; } -function getCliPlatformPackageNames(): string[] { - const variants = - process.platform === "linux" - ? process.report?.getReport().header.glibcVersionRuntime - ? ["linux", "linuxmusl"] - : ["linuxmusl", "linux"] - : [process.platform]; - return variants.map((variant) => `@github/copilot-${variant}-${process.arch}`); -} - /** Resolves the legacy SEA only for tests that explicitly exercise Node-hosted features. */ -export function getLegacyCliPathForTests(): string { - const cliName = process.platform === "win32" ? "copilot.exe" : "copilot"; - const githubModules = resolve(__dirname, "../../../node_modules/@github"); - for (const packageName of getCliPlatformPackageNames()) { - const cliPath = join(githubModules, packageName.slice("@github/".length), cliName); - if (fs.existsSync(cliPath)) { - return cliPath; - } - } - throw new Error("Legacy Copilot CLI binary not found in the installed platform package."); +export async function getLegacyCliPathForTests(): Promise { + const packageRoot = await ensureCopilotPackage(COPILOT_CLI_VERSION); + return join(packageRoot, "app.js"); } export async function createSdkTestContext({ diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts index 6366db36cb..51195206aa 100644 --- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts +++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts @@ -48,7 +48,9 @@ describe("UI Elicitation Callback", async () => { { timeout: 60_000 }, async () => { const legacyClient = ctx.createClient({ - connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + connection: RuntimeConnection.forStdio({ + path: await getLegacyCliPathForTests(), + }), }); try { const session = await legacyClient.createSession({ diff --git a/nodejs/test/runtimeArtifacts.test.ts b/nodejs/test/runtimeArtifacts.test.ts index 4a58789e6e..ffd22f21ef 100644 --- a/nodejs/test/runtimeArtifacts.test.ts +++ b/nodejs/test/runtimeArtifacts.test.ts @@ -1,9 +1,21 @@ import { existsSync, mkdtempSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { dirname, join, resolve } from "node:path"; +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { c as createTar } from "tar"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { defaultRuntimeCacheRoot, materializeRuntimeBundle } from "../src/runtimeArtifacts.js"; +import { + defaultRuntimeCacheRoot, + ensureRuntimeBundle, + getRuntimePackageName, + getRuntimePlatform, + getRuntimeReleaseAssetName, + materializeRuntimeBundle, +} from "../src/runtimeArtifacts.js"; +import { COPILOT_CLI_USE_NPM_PACKAGE, COPILOT_CLI_VERSION } from "../src/cliVersion.js"; +import { ensureCopilotPackage } from "../scripts/releaseArtifacts.js"; describe("defaultRuntimeCacheRoot", () => { it.each([ @@ -31,6 +43,66 @@ describe("defaultRuntimeCacheRoot", () => { }); }); +describe("release runtime selection", () => { + it("keeps the compiled CLI version aligned with package metadata", () => { + const packageJson = JSON.parse( + readFileSync(join(import.meta.dirname, "../package.json"), "utf8") + ); + expect(COPILOT_CLI_VERSION).toBe(packageJson.copilotCliVersion); + // lgtm[js/trivial-conditional] This generated constant is true for internal canary builds. + if (COPILOT_CLI_USE_NPM_PACKAGE) { + expect(packageJson.dependencies["@github/copilot"]).toBe(COPILOT_CLI_VERSION); + } else { + expect(packageJson.dependencies).not.toHaveProperty("@github/copilot"); + } + }); + + it("can pin an internal npm package without contacting GitHub Releases", () => { + const root = mkdtempSync(join(tmpdir(), "copilot-cli-version-")); + mkdirSync(join(root, "scripts"), { recursive: true }); + mkdirSync(join(root, "src"), { recursive: true }); + writeFileSync(join(root, "package.json"), "{}\n"); + writeFileSync( + join(root, "scripts", "set-cli-version.js"), + readFileSync(join(import.meta.dirname, "../scripts/set-cli-version.js")) + ); + + const result = spawnSync( + process.execPath, + [join(root, "scripts", "set-cli-version.js"), "9.9.9-canary.test", "--npm-package"], + { encoding: "utf8" } + ); + expect(result.status, result.stderr).toBe(0); + expect(JSON.parse(readFileSync(join(root, "package.json"), "utf8"))).toMatchObject({ + copilotCliVersion: "9.9.9-canary.test", + }); + expect(existsSync(join(root, "copilot-cli.json"))).toBe(false); + expect(readFileSync(join(root, "src", "cliVersion.ts"), "utf8")).toContain( + "COPILOT_CLI_USE_NPM_PACKAGE = true" + ); + }); + + it.each([ + ["darwin", "arm64", false, "darwin-arm64"], + ["darwin", "x64", false, "darwin-x64"], + ["linux", "arm64", false, "linux-arm64"], + ["linux", "x64", true, "linuxmusl-x64"], + ["win32", "arm64", false, "win32-arm64"], + ])("maps %s/%s to %s", (platform, arch, musl, expected) => { + expect(getRuntimePlatform(platform, arch, musl)).toBe(expected); + }); + + it("uses the platform npm tarball published in the CLI release", () => { + expect(getRuntimeReleaseAssetName("1.2.3-4", "linux-x64")).toBe( + "github-copilot-1.2.3-4-linux-x64.tgz" + ); + }); + + it("uses the SDK platform package namespace", () => { + expect(getRuntimePackageName("linux-x64")).toBe("@github/copilot-sdk-linux-x64"); + }); +}); + describe("materializeRuntimeBundle", () => { afterEach(() => vi.unstubAllEnvs()); @@ -39,18 +111,26 @@ describe("materializeRuntimeBundle", () => { const cacheRoot = join(sourceDir, "absent-cache"); const emptyPath = join(sourceDir, "empty-path"); mkdirSync(emptyPath); - const wrapperName = - process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; - const prebuilds = join(sourceDir, "prebuilds", "test-platform"); + const platform = process.platform === "win32" ? "win32-x64" : "test-platform"; + const wrapperName = platform.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; + const prebuilds = join(sourceDir, "prebuilds", platform); const wrapper = join(prebuilds, wrapperName); const runtimeNode = join(prebuilds, "runtime.node"); mkdirSync(prebuilds, { recursive: true }); writeFileSync(wrapper, "wrapper"); writeFileSync(runtimeNode, "runtime"); - mkdirSync(join(sourceDir, "ripgrep", "bin", "test-platform"), { recursive: true }); - writeFileSync(join(sourceDir, "ripgrep", "bin", "test-platform", "rg"), "ripgrep"); + mkdirSync(join(sourceDir, "ripgrep", "bin", platform), { recursive: true }); + writeFileSync(join(sourceDir, "ripgrep", "bin", platform, "rg"), "ripgrep"); mkdirSync(join(sourceDir, "definitions"), { recursive: true }); writeFileSync(join(sourceDir, "definitions", "future.json"), "{}"); + mkdirSync(join(sourceDir, "copilot-sdk"), { recursive: true }); + writeFileSync(join(sourceDir, "copilot-sdk", "extension.js"), "extension SDK"); + mkdirSync(join(sourceDir, "preloads"), { recursive: true }); + writeFileSync(join(sourceDir, "preloads", "extension_bootstrap.mjs"), "bootstrap"); + mkdirSync(join(sourceDir, "sdk"), { recursive: true }); + writeFileSync(join(sourceDir, "sdk", "index.js"), "legacy SDK"); writeFileSync(join(sourceDir, "app.js"), "excluded"); writeFileSync(join(sourceDir, "copilot"), "excluded"); writeFileSync(join(sourceDir, "copilot.exe"), "excluded"); @@ -67,16 +147,25 @@ describe("materializeRuntimeBundle", () => { expect(process.env.COPILOT_RUNTIME_PROVIDER_LIB).toBeUndefined(); const installedWrapper = materializeRuntimeBundle( - { packageRoot: sourceDir, platform: "test-platform" }, + { packageRoot: sourceDir, platform }, cacheRoot ); - const installDir = dirname(installedWrapper); + const installDir = resolve(dirname(installedWrapper), "..", ".."); expect(readFileSync(installedWrapper, "utf8")).toBe("wrapper"); - expect(readFileSync(join(installDir, "runtime.node"), "utf8")).toBe("runtime"); - expect( - readFileSync(join(installDir, "ripgrep", "bin", "test-platform", "rg"), "utf8") - ).toBe("ripgrep"); + expect(readFileSync(join(installDir, "prebuilds", platform, "runtime.node"), "utf8")).toBe( + "runtime" + ); + expect(readFileSync(join(installDir, "ripgrep", "bin", platform, "rg"), "utf8")).toBe( + "ripgrep" + ); + expect(readFileSync(join(installDir, "copilot-sdk", "extension.js"), "utf8")).toBe( + "extension SDK" + ); + expect(readFileSync(join(installDir, "preloads", "extension_bootstrap.mjs"), "utf8")).toBe( + "bootstrap" + ); + expect(readFileSync(join(installDir, "sdk", "index.js"), "utf8")).toBe("legacy SDK"); expect(existsSync(join(installDir, "app.js"))).toBe(false); expect(existsSync(join(installDir, "copilot"))).toBe(false); expect(existsSync(join(installDir, "copilot.exe"))).toBe(false); @@ -89,9 +178,11 @@ describe("materializeRuntimeBundle", () => { it("fails clearly when the package has no runtime.node", () => { const sourceDir = mkdtempSync(join(tmpdir(), "copilot-runtime-missing-node-")); - const wrapperName = - process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; - const prebuilds = join(sourceDir, "prebuilds", "test-platform"); + const platform = process.platform === "win32" ? "win32-x64" : "test-platform"; + const wrapperName = platform.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; + const prebuilds = join(sourceDir, "prebuilds", platform); const wrapper = join(prebuilds, wrapperName); mkdirSync(prebuilds, { recursive: true }); writeFileSync(wrapper, "wrapper"); @@ -100,10 +191,140 @@ describe("materializeRuntimeBundle", () => { materializeRuntimeBundle( { packageRoot: sourceDir, - platform: "test-platform", + platform, }, join(sourceDir, "cache") ) ).toThrow(/Copilot runtime\.node not found/); }); }); + +describe("ensureRuntimeBundle", () => { + it("resolves the installed platform runtime without network access", async () => { + const root = mkdtempSync(join(tmpdir(), "copilot-packaged-runtime-")); + const nodeModules = join(root, "node_modules"); + const platform = "linux-x64"; + const packageRoot = join(nodeModules, ...getRuntimePackageName(platform).split("/")); + const prebuilds = join(packageRoot, "prebuilds", platform); + mkdirSync(prebuilds, { recursive: true }); + writeFileSync(join(packageRoot, "package.json"), "{}"); + writeFileSync(join(prebuilds, "copilot-runtime"), "wrapper"); + writeFileSync(join(prebuilds, "runtime.node"), "runtime"); + mkdirSync(join(packageRoot, "schemas")); + writeFileSync(join(packageRoot, "schemas", "api.schema.json"), "{}"); + const fetcher = vi.fn(() => { + throw new Error("runtime resolution must not fetch"); + }); + vi.stubGlobal("fetch", fetcher); + + const runtimePath = await ensureRuntimeBundle(COPILOT_CLI_VERSION, { + packageSearchPaths: [nodeModules], + platform, + }); + + expect(runtimePath).toBe(join(prebuilds, "copilot-runtime")); + expect(readFileSync(join(dirname(runtimePath), "runtime.node"), "utf8")).toBe("runtime"); + expect(fetcher).not.toHaveBeenCalled(); + vi.unstubAllGlobals(); + }); + + it("fails clearly when the platform package is not installed", async () => { + await expect( + ensureRuntimeBundle(COPILOT_CLI_VERSION, { + packageSearchPaths: [], + platform: "linux-x64", + }) + ).rejects.toThrow( + "Could not resolve @github/copilot-sdk-linux-x64. Reinstall @github/copilot-sdk" + ); + }); +}); + +describe("release package acquisition", () => { + it("downloads, verifies, and caches a release package for packaging", async () => { + const sourceRoot = mkdtempSync(join(tmpdir(), "copilot-release-source-")); + const packageRoot = join(sourceRoot, "package"); + const platform = "linux-x64"; + const prebuilds = join(packageRoot, "prebuilds", platform); + mkdirSync(prebuilds, { recursive: true }); + writeFileSync(join(prebuilds, "copilot-runtime"), "wrapper"); + writeFileSync(join(prebuilds, "runtime.node"), "runtime"); + mkdirSync(join(packageRoot, "schemas"), { recursive: true }); + writeFileSync(join(packageRoot, "schemas", "api.schema.json"), "{}"); + + const archivePath = join(sourceRoot, "runtime.tgz"); + await createTar({ cwd: sourceRoot, file: archivePath, gzip: true }, ["package"]); + const archive = readFileSync(archivePath); + const version = "1.2.3-4"; + const assetName = getRuntimeReleaseAssetName(version, platform); + const checksum = createHash("sha256").update(archive).digest("hex"); + const fetcher = vi.fn(async (input: string | URL | Request) => + String(input).endsWith("/SHA256SUMS.txt") + ? new Response(`${checksum} ${assetName}\n`) + : new Response(archive) + ); + const cacheRoot = join(sourceRoot, "cache"); + + const downloadedPackage = await ensureCopilotPackage(version, { + cacheRoot, + fetch: fetcher, + platform, + }); + expect(readFileSync(join(downloadedPackage, "schemas", "api.schema.json"), "utf8")).toBe( + "{}" + ); + + await expect( + ensureCopilotPackage(version, { + cacheRoot, + fetch: fetcher, + platform, + }) + ).resolves.toBe(downloadedPackage); + expect(fetcher).toHaveBeenCalledTimes(2); + }); + + it("rejects a release package that does not match SHA256SUMS.txt", async () => { + const cacheRoot = mkdtempSync(join(tmpdir(), "copilot-release-mismatch-")); + const assetName = getRuntimeReleaseAssetName("1.2.3", "linux-x64"); + const fetcher = vi.fn(async (input: string | URL | Request) => + String(input).endsWith("/SHA256SUMS.txt") + ? new Response(`${"0".repeat(64)} ${assetName}\n`) + : new Response("corrupt archive") + ); + + await expect( + ensureCopilotPackage("1.2.3", { + cacheRoot, + fetch: fetcher, + platform: "linux-x64", + }) + ).rejects.toThrow("Checksum mismatch"); + expect(existsSync(join(cacheRoot, "1.2.3", "packages", "linux-x64"))).toBe(false); + }); + + it("times out and retries a stalled release download", async () => { + const cacheRoot = mkdtempSync(join(tmpdir(), "copilot-release-timeout-")); + const fetcher = vi.fn( + (_input: string | URL | Request, init?: RequestInit) => + new Promise((_resolve, reject) => { + const signal = init?.signal; + if (!signal) { + reject(new Error("Expected a request timeout signal.")); + return; + } + signal.addEventListener("abort", () => reject(signal.reason), { once: true }); + }) + ); + + await expect( + ensureCopilotPackage("1.2.3-timeout", { + cacheRoot, + fetch: fetcher, + fetchTimeoutMs: 10, + platform: "linux-x64", + }) + ).rejects.toThrow("Failed to download"); + expect(fetcher).toHaveBeenCalledTimes(3); + }); +}); diff --git a/python/copilot/_ffi_runtime_host.py b/python/copilot/_ffi_runtime_host.py index 98aa776600..5665f8fba7 100644 --- a/python/copilot/_ffi_runtime_host.py +++ b/python/copilot/_ffi_runtime_host.py @@ -121,7 +121,8 @@ def resolve_library_path(runtime_entrypoint: str) -> str | None: 1. The natural platform library name next to the CLI (bundled/flat layout, what the Python download-at-first-use path writes). - 2. ``prebuilds//runtime.node`` next to the CLI (dev/package layout). + 2. ``runtime.node`` next to the CLI (prepared npm runtime layout). + 3. ``prebuilds//runtime.node`` next to the CLI (package-root layout). Returns the absolute path, or ``None`` when neither exists. """ @@ -131,6 +132,10 @@ def resolve_library_path(runtime_entrypoint: str) -> str | None: if flat.is_file(): return str(flat) + adjacent_prebuilt = directory / "runtime.node" + if adjacent_prebuilt.is_file(): + return str(adjacent_prebuilt) + folder = get_prebuilds_folder() if folder is not None: prebuilt = directory / "prebuilds" / folder / "runtime.node" diff --git a/python/copilot/_jsonrpc.py b/python/copilot/_jsonrpc.py index 6427a8007f..11baf0bd10 100644 --- a/python/copilot/_jsonrpc.py +++ b/python/copilot/_jsonrpc.py @@ -296,29 +296,31 @@ def _read_loop(self): def _fail_pending_requests(self): """Fail all pending requests when process exits""" + error_msg = self._get_process_exit_error() + + # Fail all pending requests + with self._pending_lock: + for request_id, future in list(self.pending_requests.items()): + if not future.done(): + exc = ProcessExitedError(error_msg) + loop = future.get_loop() + loop.call_soon_threadsafe(future.set_exception, exc) + + def _get_process_exit_error(self) -> str: + """Build an error message after the process and stderr readers finish.""" if self._stderr_thread and self._stderr_thread is not threading.current_thread(): self._stderr_thread.join(timeout=1.0) - # Build error message with stderr output stderr_output = self.get_stderr_output() return_code = None if hasattr(self.process, "poll"): return_code = self.process.poll() if stderr_output: - error_msg = f"CLI process exited with code {return_code}\nstderr: {stderr_output}" - elif return_code is not None: - error_msg = f"CLI process exited with code {return_code}" - else: - error_msg = "CLI process exited unexpectedly" - - # Fail all pending requests - with self._pending_lock: - for request_id, future in list(self.pending_requests.items()): - if not future.done(): - exc = ProcessExitedError(error_msg) - loop = future.get_loop() - loop.call_soon_threadsafe(future.set_exception, exc) + return f"CLI process exited with code {return_code}\nstderr: {stderr_output}" + if return_code is not None: + return f"CLI process exited with code {return_code}" + return "CLI process exited unexpectedly" def _read_exact(self, num_bytes: int) -> bytes: """ diff --git a/python/copilot/client.py b/python/copilot/client.py index 929194c9b7..f4d7da75a7 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -2012,13 +2012,14 @@ async def start(self) -> None: # Check if process exited and capture any remaining stderr process = self._cli_process if self._cli_process is not None else self._process if process and hasattr(process, "poll"): + if isinstance(e, BrokenPipeError) and process.poll() is None: + try: + await asyncio.to_thread(process.wait, timeout=1.0) + except subprocess.TimeoutExpired: + pass return_code = process.poll() if return_code is not None and self._client: - stderr_output = self._client.get_stderr_output() - if stderr_output: - raise RuntimeError( - f"CLI process exited with code {return_code}\nstderr: {stderr_output}" - ) from e + raise RuntimeError(self._client._get_process_exit_error()) from e raise async def stop(self) -> None: @@ -4792,7 +4793,10 @@ async def _apply_post_create_options_patch( try: await session.disconnect() except BaseException: - pass + logger.debug( + "Error disconnecting session after options update failure", + exc_info=True, + ) raise async def _set_session_fs_provider(self) -> None: diff --git a/python/e2e/conftest.py b/python/e2e/conftest.py index a789b2b567..d61fe0d875 100644 --- a/python/e2e/conftest.py +++ b/python/e2e/conftest.py @@ -20,10 +20,8 @@ # Out-of-process children resolve auth in their own process where the token already # outranks HMAC. See https://github.com/github/copilot-sdk/issues/1934. if not cli_download.CLI_VERSION: - package_lock = json.loads( - (Path(__file__).parents[2] / "nodejs" / "package-lock.json").read_text() - ) - cli_download.CLI_VERSION = package_lock["packages"]["node_modules/@github/copilot"]["version"] + package_json = json.loads((Path(__file__).parents[2] / "nodejs" / "package.json").read_text()) + cli_download.CLI_VERSION = package_json["copilotCliVersion"] if is_inprocess_transport(): os.environ.pop("COPILOT_HMAC_KEY", None) diff --git a/python/e2e/test_event_fidelity_e2e.py b/python/e2e/test_event_fidelity_e2e.py index 25b18407a0..6b14ab2486 100644 --- a/python/e2e/test_event_fidelity_e2e.py +++ b/python/e2e/test_event_fidelity_e2e.py @@ -46,7 +46,7 @@ async def test_should_emit_events_in_correct_order_for_tool_using_conversation( assert user_idx < assistant_idx idle_idx = len(types) - 1 - types[::-1].index("session.idle") - assert idle_idx == len(types) - 1 + assert assistant_idx < idle_idx finally: unsubscribe() await session.disconnect() diff --git a/python/e2e/test_rpc_server_misc_e2e.py b/python/e2e/test_rpc_server_misc_e2e.py index d5ade1aec1..2b6b7d9514 100644 --- a/python/e2e/test_rpc_server_misc_e2e.py +++ b/python/e2e/test_rpc_server_misc_e2e.py @@ -131,8 +131,11 @@ async def test_should_login_list_get_current_auth_and_logout_account(self, ctx: client, home = await _create_isolated_client(ctx, github_token=None) try: - initial = await client.rpc.account.get_current_auth() - assert initial.auth_info is None + assert client._client is not None + initial_current = await client._client.request("account.getCurrentAuth", {}) + initial_auth_info = initial_current.get("authInfo") + assert initial_auth_info is not None + assert initial_auth_info.get("login") != login login_result = await client.rpc.account.login( AccountLoginRequest(host="https://github.com", login=login, token=token) @@ -152,21 +155,26 @@ async def test_should_login_list_get_current_auth_and_logout_account(self, ctx: ( user for user in users - if user.auth_info.type == "user" - and getattr(user.auth_info, "login", None) == login + if user.get("authInfo", {}).get("type") == "user" + and user.get("authInfo", {}).get("login") == login ), None, ) if account is not None: - assert account.token == token + assert account.get("token") == token logout = await client.rpc.account.logout( AccountLogoutRequest(auth_info=current.auth_info) ) assert logout.has_more_users is False - after_logout = await client.rpc.account.get_current_auth() - assert after_logout.auth_info is None + users_after_logout = await client.rpc.account.get_all_users() + assert all( + user.get("authInfo", {}).get("login") != login for user in users_after_logout + ) + + current_after_logout = await client._client.request("account.getCurrentAuth", {}) + assert current_after_logout.get("authInfo") == initial_auth_info finally: await _dispose_isolated(client, home) diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index 8eaecf6244..616fa9bb0a 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -9,91 +9,47 @@ import os import re import shutil +import subprocess import tempfile import time -from collections.abc import Sequence from pathlib import Path from typing import Any from copilot import CopilotClient, RuntimeConnection -from copilot._cli_version import get_npm_platform from .proxy import CapiProxy -def _cli_platform_package_names(npm_platform: str | None = None) -> list[str]: - """Return candidate ``@github/copilot-*`` directory names, best match first. - - Mirrors ``getCliPlatformPackageNames()`` in ``nodejs/src/client.ts``: as of CLI - 1.0.64-1 the runnable ``index.js`` ships in a platform package such as - ``copilot-darwin-arm64``. On Linux both libc variants are listed (the detected - one first) because npm installs exactly one of them and musl probing can come up - empty in minimal containers. - """ - primary = npm_platform or get_npm_platform() - names = [f"copilot-{primary}"] - if primary.startswith("linux"): - arch = primary.rsplit("-", 1)[-1] - for variant in (f"linux-{arch}", f"linuxmusl-{arch}"): - name = f"copilot-{variant}" - if name not in names: - names.append(name) - return names - - -def _find_cli_in_node_modules(github_modules: Path, package_names: Sequence[str]) -> str | None: - """Return the resolved ``index.js`` of the first installed candidate package. - - Only exact package names are probed, so unrelated ``copilot-*`` directories - (e.g. ``copilot-language-server``) can never be mistaken for the CLI. - """ - for name in package_names: - candidate = github_modules / name / "index.js" - if candidate.exists(): - return str(candidate.resolve()) - return None - - -def _installed_cli_package_names(github_modules: Path) -> list[str]: - """Return the ``copilot-*`` directory names present, for error messages only. - - Selection never globs — that was the #2103 bug. This exists so a failure can - say what *is* installed, which is the difference between a dead-end "run npm - install" and a message that diagnoses itself on a mixed-architecture host. - """ - if not github_modules.is_dir(): - return [] - return sorted(path.name for path in github_modules.glob("copilot-*") if path.is_dir()) +def _prepare_pinned_cli(repo_root: Path) -> str: + npm = "npm.cmd" if os.name == "nt" else "npm" + result = subprocess.run( + [npm, "run", "--silent", "prepare:runtime", "--", "--print-path"], + cwd=repo_root / "nodejs", + capture_output=True, + text=True, + check=False, + ) + output = result.stdout.strip() + if result.returncode != 0 or not output: + detail = result.stderr.strip() or output or f"exit code {result.returncode}" + raise RuntimeError(f"Failed to prepare the pinned Copilot CLI: {detail}") + cli_path = Path(output.splitlines()[-1]) + if not cli_path.is_file(): + raise RuntimeError(f"Pinned Copilot CLI was not created at {cli_path}") + return str(cli_path.resolve()) def get_cli_path_for_tests() -> str: """Get CLI path for E2E tests. - Uses COPILOT_CLI_PATH env var if set, otherwise the platform-specific CLI - package in the sibling nodejs directory's node_modules. + Uses COPILOT_CLI_PATH env var if set, otherwise prepares the release pinned + by the sibling Node.js SDK. """ env_path = os.environ.get("COPILOT_CLI_PATH") if env_path and Path(env_path).exists(): return str(Path(env_path).resolve()) - # 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), so pick the - # one built for this host rather than whichever sorts first (#2103). - base_path = Path(__file__).parents[3] - github_modules = base_path / "nodejs" / "node_modules" / "@github" - package_names = _cli_platform_package_names() - found = _find_cli_in_node_modules(github_modules, package_names) - if found is not None: - return found - - installed = _installed_cli_package_names(github_modules) - raise RuntimeError( - f"CLI not found for tests under {github_modules} " - f"(tried: {', '.join(package_names)}; " - f"present: {', '.join(installed) or 'none'}). " - "Run 'npm install' in the nodejs directory, or set COPILOT_CLI_PATH." - ) + return _prepare_pinned_cli(Path(__file__).parents[3]) CLI_PATH = get_cli_path_for_tests() diff --git a/python/scripts/inject-cli-version.mjs b/python/scripts/inject-cli-version.mjs index 359e7f680b..fef0f967b8 100644 --- a/python/scripts/inject-cli-version.mjs +++ b/python/scripts/inject-cli-version.mjs @@ -2,7 +2,7 @@ /** * inject-cli-version.mjs * - * Reads the pinned @github/copilot version from nodejs/package-lock.json and + * Reads the pinned Copilot CLI version from nodejs/package.json and * writes it into python/copilot/_cli_version.py, replacing the `CLI_VERSION = None` * sentinel with the concrete version string. * @@ -17,19 +17,13 @@ import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = join(__dirname, "..", ".."); -// Read version from nodejs/package-lock.json -const lockPath = join(repoRoot, "nodejs", "package-lock.json"); -const lock = JSON.parse(readFileSync(lockPath, "utf-8")); - -// The version is in packages["node_modules/@github/copilot"].version -const copilotPkg = lock.packages?.["node_modules/@github/copilot"]; -if (!copilotPkg?.version) { - console.error( - "Error: Could not find @github/copilot version in nodejs/package-lock.json" - ); +const packagePath = join(repoRoot, "nodejs", "package.json"); +const packageJson = JSON.parse(readFileSync(packagePath, "utf-8")); +const version = packageJson.copilotCliVersion; +if (!version) { + console.error("Error: Could not find copilotCliVersion in nodejs/package.json"); process.exit(1); } -const version = copilotPkg.version; console.log(`Injecting CLI_VERSION = "${version}"`); // Patch _cli_version.py diff --git a/python/test_cli_download.py b/python/test_cli_download.py index a5a20dce0d..aac652daf8 100644 --- a/python/test_cli_download.py +++ b/python/test_cli_download.py @@ -11,7 +11,7 @@ import pytest -from copilot import _cli_download +from copilot import _cli_download, _ffi_runtime_host def _integrity(data: bytes, algo: str = "sha512") -> str: @@ -78,6 +78,15 @@ def test_raises_when_integrity_unavailable(self, tmp_path): extract.assert_not_called() +def test_resolve_library_path_accepts_adjacent_runtime_node(tmp_path): + wrapper = tmp_path / ("copilot-runtime.exe" if os.name == "nt" else "copilot-runtime") + wrapper.write_bytes(b"wrapper") + runtime_node = tmp_path / "runtime.node" + runtime_node.write_bytes(b"runtime") + + assert _ffi_runtime_host.resolve_library_path(str(wrapper)) == str(runtime_node) + + class TestEnsureRuntimeWrapper: def test_materializes_pair_from_absent_cache_with_stripped_environment( self, tmp_path, monkeypatch diff --git a/python/test_e2e_harness_cli_path.py b/python/test_e2e_harness_cli_path.py index 8a50ba7a51..83167ffd33 100644 --- a/python/test_e2e_harness_cli_path.py +++ b/python/test_e2e_harness_cli_path.py @@ -1,99 +1,12 @@ -"""Unit tests for the E2E harness's Copilot CLI platform-package resolution. - -Regression coverage for github/copilot-sdk#2103: the harness used to return the -first ``@github/copilot-*`` directory in alphabetical order instead of the package -built for the current platform. -""" +"""Unit tests for the E2E harness's pinned CLI preparation.""" from __future__ import annotations -from pathlib import Path - import pytest -from copilot._cli_version import get_npm_platform from e2e.testharness import context -def _make_package(github_modules: Path, name: str) -> Path: - """Create ``//index.js`` and return the entrypoint path.""" - package_dir = github_modules / name - package_dir.mkdir(parents=True, exist_ok=True) - index = package_dir / "index.js" - index.write_text("// fake CLI entrypoint\n") - return index - - -class TestCliPlatformPackageNames: - def test_non_linux_platform_yields_single_candidate(self): - assert context._cli_platform_package_names("darwin-arm64") == ["copilot-darwin-arm64"] - - def test_windows_platform_yields_single_candidate(self): - assert context._cli_platform_package_names("win32-x64") == ["copilot-win32-x64"] - - def test_glibc_linux_also_considers_musl_variant(self): - assert context._cli_platform_package_names("linux-x64") == [ - "copilot-linux-x64", - "copilot-linuxmusl-x64", - ] - - def test_musl_linux_prefers_musl_then_falls_back_to_glibc(self): - assert context._cli_platform_package_names("linuxmusl-arm64") == [ - "copilot-linuxmusl-arm64", - "copilot-linux-arm64", - ] - - def test_defaults_to_current_host_platform(self): - assert context._cli_platform_package_names()[0] == f"copilot-{get_npm_platform()}" - - -class TestFindCliInNodeModules: - def test_skips_alphabetically_earlier_foreign_package(self, tmp_path): - # The #2103 regression: "aardvark" sorts before every real platform name. - _make_package(tmp_path, "copilot-aardvark-x64") - expected = _make_package(tmp_path, "copilot-darwin-arm64") - found = context._find_cli_in_node_modules(tmp_path, ["copilot-darwin-arm64"]) - assert found == str(expected.resolve()) - - def test_returns_none_when_no_candidate_is_installed(self, tmp_path): - _make_package(tmp_path, "copilot-win32-x64") - assert context._find_cli_in_node_modules(tmp_path, ["copilot-darwin-arm64"]) is None - - def test_ignores_non_platform_copilot_packages(self, tmp_path): - _make_package(tmp_path, "copilot-language-server") - assert context._find_cli_in_node_modules(tmp_path, ["copilot-linux-x64"]) is None - - def test_prefers_earlier_candidate_when_both_libc_variants_exist(self, tmp_path): - expected = _make_package(tmp_path, "copilot-linuxmusl-x64") - _make_package(tmp_path, "copilot-linux-x64") - found = context._find_cli_in_node_modules( - tmp_path, ["copilot-linuxmusl-x64", "copilot-linux-x64"] - ) - assert found == str(expected.resolve()) - - def test_returns_none_when_package_dir_has_no_index_js(self, tmp_path): - (tmp_path / "copilot-linux-x64").mkdir() - assert context._find_cli_in_node_modules(tmp_path, ["copilot-linux-x64"]) is None - - def test_returns_none_when_github_modules_is_absent(self, tmp_path): - missing = tmp_path / "missing" - assert context._find_cli_in_node_modules(missing, ["copilot-linux-x64"]) is None - - -class TestInstalledCliPackageNames: - def test_lists_platform_directories_sorted(self, tmp_path): - _make_package(tmp_path, "copilot-win32-x64") - _make_package(tmp_path, "copilot-darwin-arm64") - (tmp_path / "not-copilot").mkdir() - assert context._installed_cli_package_names(tmp_path) == [ - "copilot-darwin-arm64", - "copilot-win32-x64", - ] - - def test_returns_empty_when_directory_is_absent(self, tmp_path): - assert context._installed_cli_package_names(tmp_path / "missing") == [] - - class TestGetCliPathForTests: def test_env_var_takes_precedence(self, tmp_path, monkeypatch): cli = tmp_path / "custom-cli.js" @@ -101,46 +14,26 @@ def test_env_var_takes_precedence(self, tmp_path, monkeypatch): monkeypatch.setenv("COPILOT_CLI_PATH", str(cli)) assert context.get_cli_path_for_tests() == str(cli.resolve()) - def test_error_names_the_packages_tried_and_the_remedy(self, monkeypatch): + def test_prepares_the_pinned_runtime(self, tmp_path, monkeypatch): monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) - monkeypatch.setattr( - context, "_cli_platform_package_names", lambda *_: ["copilot-linux-x64"] - ) - monkeypatch.setattr(context, "_find_cli_in_node_modules", lambda *_: None) - with pytest.raises(RuntimeError) as excinfo: - context.get_cli_path_for_tests() - message = str(excinfo.value) - assert "copilot-linux-x64" in message - assert "npm install" in message - assert "COPILOT_CLI_PATH" in message + cli = tmp_path / "copilot" + cli.write_text("runtime\n") - def test_error_names_the_searched_directory(self, monkeypatch): - monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) - seen: list[Path] = [] + class Result: + returncode = 0 + stdout = f"{cli}\n" + stderr = "" - def fake_find(github_modules, package_names): - seen.append(github_modules) - return None + monkeypatch.setattr(context.subprocess, "run", lambda *args, **kwargs: Result()) + assert context._prepare_pinned_cli(tmp_path) == str(cli.resolve()) - monkeypatch.setattr(context, "_cli_platform_package_names", lambda *_: ["copilot-nope-x64"]) - monkeypatch.setattr(context, "_find_cli_in_node_modules", fake_find) - with pytest.raises(RuntimeError) as excinfo: - context.get_cli_path_for_tests() - assert seen, "get_cli_path_for_tests must consult _find_cli_in_node_modules" - assert seen[0].name == "@github" - assert seen[0].parent.name == "node_modules" - assert seen[0].parent.parent.name == "nodejs" - assert str(seen[0]) in str(excinfo.value) + def test_preparation_failure_includes_command_error(self, tmp_path, monkeypatch): + class Result: + returncode = 1 + stdout = "" + stderr = "download failed" - def test_error_lists_the_packages_actually_installed(self, monkeypatch): - monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) - monkeypatch.setattr(context, "_cli_platform_package_names", lambda *_: ["copilot-nope-x64"]) - monkeypatch.setattr(context, "_find_cli_in_node_modules", lambda *_: None) - monkeypatch.setattr( - context, "_installed_cli_package_names", lambda *_: ["copilot-darwin-arm64"] - ) + monkeypatch.setattr(context.subprocess, "run", lambda *args, **kwargs: Result()) with pytest.raises(RuntimeError) as excinfo: - context.get_cli_path_for_tests() - message = str(excinfo.value) - assert "present: copilot-darwin-arm64" in message - assert "copilot-nope-x64" in message + context._prepare_pinned_cli(tmp_path) + assert "download failed" in str(excinfo.value) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c7a704fd50..c66480d511 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -119,7 +119,6 @@ test = false bench = false [build-dependencies] -base64 = "0.22" dirs = "5" flate2 = "1" serde_json = "1" diff --git a/rust/README.md b/rust/README.md index b9a1f5b1bc..a561e6da09 100644 --- a/rust/README.md +++ b/rust/README.md @@ -961,12 +961,12 @@ github-copilot-sdk = { version = "0.1", default-features = false } 1. **Version pin.** `build.rs` reads the CLI version from one of two sources: - `cli-version.txt` at the crate root (present in published crate tarballs and vendored slots). - - Otherwise, `../nodejs/package-lock.json` (contributor build inside the github/copilot-sdk repo — matches the .NET and Go SDK conventions here). + - Otherwise, `../nodejs/package.json` (contributor build inside the github/copilot-sdk repo). The resolved version is baked into the crate via `cargo:rustc-env=COPILOT_SDK_CLI_VERSION` regardless of mode. The runtime resolver consumes it to recompute the on-disk path by convention, so no absolute paths leak into the rlib. -2. **Build time:** `build.rs` downloads the platform-specific npm package and - verifies its `sha512` integrity against the lockfile or publish snapshot. +2. **Build time:** `build.rs` downloads the platform-specific release archive and + verifies its SHA-256 against the release's `SHA256SUMS.txt` or the publish snapshot. Then: - **`bundled-cli` on (default):** creates and embeds a minimal archive containing the CLI executable, `copilot-runtime[.exe]`, and `runtime.node`. - **`bundled-in-process` on:** the archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`). diff --git a/rust/build/in_process.rs b/rust/build/in_process.rs index e6edcf1b90..c01e7ffc4f 100644 --- a/rust/build/in_process.rs +++ b/rust/build/in_process.rs @@ -2,7 +2,6 @@ use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::time::Duration; -use base64::Engine; use sha2::Digest; pub(crate) fn main() { @@ -14,20 +13,20 @@ pub(crate) fn main() { println!("cargo::rustc-check-cfg=cfg(has_extracted_cli)"); println!("cargo:rerun-if-changed=cli-version-in-process.txt"); - // Only declare the lockfile rerun when the lockfile actually exists. + // Only declare the package metadata rerun when it actually exists. // Cargo treats `rerun-if-changed` for a missing path as "always rerun" // — so unconditionally declaring this on consumers without a sibling // `nodejs/` (vendored slots, published crates) would force build.rs // to re-run on every `cargo build` even when nothing has changed. - // The lockfile path is only the source-of-truth in this repo's + // The package file is only the source-of-truth in this repo's // contributor builds; everywhere else `cli-version-in-process.txt` is canonical. let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); - let lockfile = Path::new(&manifest_dir) + let package_json = Path::new(&manifest_dir) .join("..") .join("nodejs") - .join("package-lock.json"); - if lockfile.is_file() { - println!("cargo:rerun-if-changed={}", lockfile.display()); + .join("package.json"); + if package_json.is_file() { + println!("cargo:rerun-if-changed={}", package_json.display()); } // Hard opt-out: disable the entire download / bundle / cache mechanism @@ -60,17 +59,13 @@ pub(crate) fn main() { let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is always set by cargo"); let out = Path::new(&out_dir); - // Resolve version + npm integrity from one of two sources, in order: + // Resolve version and, when available locally, the release SHA-256 from + // one of two sources, in order: // 1. `cli-version-in-process.txt` snapshot at the crate root (published-crate - // consumer; generated by the publish workflow). Combined format: - // `version=X` line + per-package integrity lines. Committing these - // makes the publish workflow the trust boundary — an attacker who - // later re-points the release tag can't silently poison consumer - // builds. - // 2. Sibling `../nodejs/package-lock.json` (contributor build inside - // the github/copilot-sdk repo), whose platform-package integrity is - // the same trust source npm uses. - let (version, expected_integrity) = resolve_version_and_integrity(platform.package_name); + // consumer; generated by the publish workflow from SHA256SUMS.txt). + // 2. Sibling `../nodejs/package.json` plus the release SHA256SUMS.txt + // (contributor build inside the github/copilot-sdk repo). + let (version, local_expected_hash) = resolve_version_and_optional_hash(platform.package_name); // Bake the version into the crate regardless of mode. This is the // single source of truth for "what CLI version did build.rs target", @@ -81,10 +76,13 @@ pub(crate) fn main() { // `target/` reuse stays cache-coherent. println!("cargo:rustc-env=COPILOT_SDK_CLI_VERSION={version}"); - let archive_name = format!("{}-{version}.tgz", platform.package_name); + let asset_platform = platform + .package_name + .strip_prefix("copilot-") + .expect("platform package names start with copilot-"); + let archive_name = format!("github-copilot-{version}-{asset_platform}.tgz"); let download_url = format!( - "https://registry.npmjs.org/@github/{}/-/{}", - platform.package_name, archive_name + "https://github.com/github/copilot-cli/releases/download/v{version}/{archive_name}" ); let cache_dir = std::env::var("BUNDLED_CLI_CACHE_DIR") .ok() @@ -94,7 +92,10 @@ pub(crate) fn main() { let include_runtime = std::env::var_os("CARGO_FEATURE_BUNDLED_IN_PROCESS").is_some(); if std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_some() { - let archive = cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir); + let expected_hash = local_expected_hash + .clone() + .unwrap_or_else(|| fetch_in_process_release_hash(&version, platform.package_name)); + let archive = cached_download(&download_url, &cache_key, &expected_hash, &cache_dir); verify_runtime_package(&archive, platform, &archive_name); emit_embedded(out, &archive, platform, include_runtime); println!("cargo:rustc-cfg=has_bundled_cli"); @@ -112,8 +113,6 @@ pub(crate) fn main() { install_dir.join("runtime.node"), install_dir.join(".hostless-runtime-assets-v1"), ]; - let expected_marker = format!("{version}\n{expected_integrity}\n"); - // Invalidate build.rs whenever either cached artifact disappears (cache // GC, manual rm, OS reset, switching extract dir). Without this, cargo // replays the saved `has_extracted_cli` cfg from its build-script @@ -123,10 +122,20 @@ pub(crate) fn main() { println!("cargo:rerun-if-changed={}", path.display()); } + let marker = std::fs::read_to_string(&required_paths[2]).ok(); let cache_is_current = required_paths.iter().all(|path| path.is_file()) - && std::fs::read_to_string(&required_paths[2]).ok().as_deref() - == Some(expected_marker.as_str()); + && match local_expected_hash.as_deref() { + Some(expected_hash) => { + marker.as_deref() == Some(&format!("{version}\n{expected_hash}\n")) + } + None => marker + .as_deref() + .is_some_and(|contents| marker_matches_version(contents, &version)), + }; if !cache_is_current { + let expected_hash = local_expected_hash + .unwrap_or_else(|| fetch_in_process_release_hash(&version, platform.package_name)); + let expected_marker = format!("{version}\n{expected_hash}\n"); if install_dir.exists() { std::fs::remove_dir_all(&install_dir).unwrap_or_else(|e| { panic!( @@ -135,8 +144,7 @@ pub(crate) fn main() { ) }); } - let archive = - cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir); + let archive = cached_download(&download_url, &cache_key, &expected_hash, &cache_dir); verify_runtime_package(&archive, platform, &archive_name); extract_to_cache( &archive, @@ -199,7 +207,8 @@ fn build_embedded_archive(package: &[u8], platform: Platform, include_runtime: b .mtime(0) .write(Vec::new(), flate2::Compression::default()); let mut archive = tar::Builder::new(encoder); - let runtime = append_hostless_runtime_tree(&mut archive, package, platform); + let (runtime, wrapper) = append_hostless_runtime_tree(&mut archive, package, platform); + append_archive_file(&mut archive, platform.binary_name, &wrapper, 0o755); if include_runtime { append_archive_file( &mut archive, @@ -220,10 +229,11 @@ fn append_hostless_runtime_tree( archive: &mut tar::Builder, package: &[u8], platform: Platform, -) -> Vec { +) -> (Vec, Vec) { let decoder = flate2::read::GzDecoder::new(package); let mut source = tar::Archive::new(decoder); let mut runtime = None; + let mut wrapper = None; for entry in source .entries() .unwrap_or_else(|e| panic!("failed to read npm package entries: {e}")) @@ -247,6 +257,9 @@ fn append_hostless_runtime_tree( if destination == Path::new("runtime.node") { runtime = Some(bytes.clone()); } + if destination == Path::new(platform.runtime_wrapper_name()) { + wrapper = Some(bytes.clone()); + } append_archive_file( archive, destination @@ -256,12 +269,21 @@ fn append_hostless_runtime_tree( mode, ); } - runtime.unwrap_or_else(|| { - panic!( - "package `{}` does not contain prebuilds//runtime.node", - platform.package_name - ) - }) + ( + runtime.unwrap_or_else(|| { + panic!( + "package `{}` does not contain prebuilds//runtime.node", + platform.package_name + ) + }), + wrapper.unwrap_or_else(|| { + panic!( + "package `{}` does not contain prebuilds//{}", + platform.package_name, + platform.runtime_wrapper_name() + ) + }), + ) } fn hostless_runtime_path(source: &str, platform: Platform) -> Option { @@ -276,18 +298,15 @@ fn hostless_runtime_path(source: &str, platform: Platform) -> Option { "app.js", "assets", "changelog.json", - "copilot-sdk", "foundry-local-sdk", "index.js", "LICENSE.md", "napi-oop-runtime", "npm-loader.js", "package.json", - "preloads", "pvrecorder", "queries", "README.md", - "sdk", "sea-loader.js", "webview", ]; @@ -331,10 +350,10 @@ fn append_archive_file( .unwrap_or_else(|e| panic!("failed to add `{path}` to embedded CLI archive: {e}")); } -/// Resolve the CLI version and npm integrity for the current target's -/// platform package. Picks one of two sources in order. Panics with a clear -/// error if neither is available. -fn resolve_version_and_integrity(package_name: &str) -> (String, String) { +/// Resolve the CLI version and any locally snapshotted release hash for the +/// current target's platform package. Contributor builds defer fetching the +/// checksum until a download is actually required. +fn resolve_version_and_optional_hash(package_name: &str) -> (String, Option) { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); // 1. Snapshot file at the crate root (published-crate consumer, @@ -343,17 +362,19 @@ fn resolve_version_and_integrity(package_name: &str) -> (String, String) { if snapshot.is_file() { let contents = std::fs::read_to_string(&snapshot) .unwrap_or_else(|e| panic!("failed to read {}: {e}", snapshot.display())); - return parse_snapshot(&contents, package_name) + let (version, hash) = parse_snapshot(&contents, package_name) .unwrap_or_else(|e| panic!("invalid {}: {e}", snapshot.display())); + return (version, Some(hash)); } - // 2. Lockfile fallback (contributor build inside github/copilot-sdk). - let lockfile = Path::new(&manifest_dir) + // 2. Package version plus release checksums (contributor build). + let package_json = Path::new(&manifest_dir) .join("..") .join("nodejs") - .join("package-lock.json"); - if lockfile.is_file() { - return read_version_and_integrity_from_package_lock(&lockfile, package_name); + .join("package.json"); + if package_json.is_file() { + let version = read_version_from_package_json(&package_json); + return (version, None); } panic!( @@ -362,19 +383,36 @@ fn resolve_version_and_integrity(package_name: &str) -> (String, String) { - {} (missing)\n\ - {} (missing)\n\ In a published crate or vendored slot, `cli-version-in-process.txt` should be present.\n\ - Inside the github/copilot-sdk repo, `../nodejs/package-lock.json` is the source.", + Inside the github/copilot-sdk repo, `../nodejs/package.json` is the version source.", snapshot.display(), - lockfile.display(), + package_json.display(), ); } +fn fetch_in_process_release_hash(version: &str, package_name: &str) -> String { + let platform = package_name + .strip_prefix("copilot-") + .expect("platform package names start with copilot-"); + let asset_name = format!("github-copilot-{version}-{platform}.tgz"); + fetch_release_hash(version, &asset_name) +} + +fn marker_matches_version(contents: &str, version: &str) -> bool { + let mut lines = contents.lines(); + lines.next() == Some(version) + && lines.next().is_some_and(|hash| { + hash.len() == 64 && hash.bytes().all(|byte| byte.is_ascii_hexdigit()) + }) + && lines.next().is_none() +} + /// Parse the `cli-version-in-process.txt` snapshot file. Format is one `key=value` per /// line. The first non-comment line is `version=X.Y.Z`; subsequent lines map -/// platform package name to npm integrity. Blank lines and lines starting with `#` +/// platform package name to SHA-256. Blank lines and lines starting with `#` /// are skipped. fn parse_snapshot(contents: &str, package_name: &str) -> Result<(String, String), String> { let mut version: Option = None; - let mut integrity: Option = None; + let mut hash: Option = None; for (line_no, raw) in contents.lines().enumerate() { let line = raw.trim(); if line.is_empty() || line.starts_with('#') { @@ -388,33 +426,42 @@ fn parse_snapshot(contents: &str, package_name: &str) -> Result<(String, String) }; match key.trim() { "version" => version = Some(value.trim().to_string()), - k if k == package_name => integrity = Some(value.trim().to_string()), + k if k == package_name => hash = Some(value.trim().to_string()), _ => {} } } let version = version.ok_or("missing `version=` line")?; - let integrity = - integrity.ok_or_else(|| format!("missing integrity for package `{package_name}`"))?; - Ok((version, integrity)) + let hash = hash.ok_or_else(|| format!("missing hash for package `{package_name}`"))?; + Ok((version, hash)) } -fn read_version_and_integrity_from_package_lock( - path: &Path, - package_name: &str, -) -> (String, String) { +fn read_version_from_package_json(path: &Path) -> String { let contents = std::fs::read_to_string(path) .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())); - let lock: serde_json::Value = serde_json::from_str(&contents) + let package_json: serde_json::Value = serde_json::from_str(&contents) .unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display())); - let cli_key = "node_modules/@github/copilot"; - let version = lock["packages"][cli_key]["version"] + package_json["copilotCliVersion"] .as_str() - .unwrap_or_else(|| panic!("{cli_key} has no version in {}", path.display())); - let platform_key = format!("node_modules/@github/{package_name}"); - let integrity = lock["packages"][&platform_key]["integrity"] - .as_str() - .unwrap_or_else(|| panic!("{platform_key} has no integrity in {}", path.display())); - (version.to_string(), integrity.to_string()) + .unwrap_or_else(|| panic!("copilotCliVersion is missing in {}", path.display())) + .to_string() +} + +fn fetch_release_hash(version: &str, asset_name: &str) -> String { + let url = format!( + "https://github.com/github/copilot-cli/releases/download/v{version}/SHA256SUMS.txt" + ); + let checksums = download_with_retry(&url); + let checksums = std::str::from_utf8(&checksums).expect("SHA256SUMS.txt is not valid UTF-8"); + find_sha256_for_asset(checksums, asset_name) +} + +fn find_sha256_for_asset(sums: &str, asset_name: &str) -> String { + sums.lines() + .find_map(|line| { + let (hash, name) = line.split_once(char::is_whitespace)?; + (name.trim_start().trim_start_matches('*') == asset_name).then(|| hash.to_string()) + }) + .unwrap_or_else(|| panic!("SHA256SUMS.txt does not contain {asset_name}")) } #[derive(Clone, Copy)] @@ -695,20 +742,20 @@ fn sanitize_version(version: &str) -> String { } /// Read a file from the download cache, or download it (with retries) and save -/// to cache. Verifies npm integrity on every path. Evicts stale/corrupt cache entries +/// to cache. Verifies SHA-256 on every path. Evicts stale/corrupt cache entries /// automatically. Cache I/O failures are treated as cache misses — they never /// break the build. fn cached_download( url: &str, cache_key: &str, - expected_integrity: &str, + expected_hash: &str, cache_dir: &Option, ) -> Vec { if let Some(dir) = cache_dir { let cached_path = dir.join(cache_key); if cached_path.is_file() { match std::fs::read(&cached_path) { - Ok(data) if verify_integrity(&data, expected_integrity) => { + Ok(data) if verify_hash(&data, expected_hash) => { // Silent cache hit — nothing to surface. return data; } @@ -728,9 +775,9 @@ fn cached_download( println!("cargo:warning=Downloading {url}"); let data = download_with_retry(url); - if !verify_integrity(&data, expected_integrity) { + if !verify_hash(&data, expected_hash) { panic!( - "Archive integrity check failed for {url}!\n expected: {expected_integrity}\n \ + "Archive integrity check failed for {url}!\n expected: {expected_hash}\n \ This could indicate a corrupted download or a supply-chain attack." ); } @@ -832,11 +879,7 @@ fn try_download(url: &str) -> Result, DownloadError> { } fn verify_runtime_package(archive: &[u8], platform: Platform, package_name: &str) { - for file_name in [ - platform.binary_name, - "runtime.node", - platform.runtime_wrapper_name(), - ] { + for file_name in ["runtime.node", platform.runtime_wrapper_name()] { if archive_contains_tar_entry(archive, file_name) { continue; } @@ -864,14 +907,8 @@ fn archive_contains_tar_entry(targz: &[u8], binary_name: &str) -> bool { false } -fn verify_integrity(data: &[u8], integrity: &str) -> bool { - let Some(encoded) = integrity.strip_prefix("sha512-") else { - return false; - }; - let Ok(expected) = base64::engine::general_purpose::STANDARD.decode(encoded) else { - return false; - }; - let mut hasher = sha2::Sha512::new(); +fn verify_hash(data: &[u8], expected: &str) -> bool { + let mut hasher = sha2::Sha256::new(); hasher.update(data); - hasher.finalize().as_slice() == expected + format!("{:x}", hasher.finalize()) == expected } diff --git a/rust/build/out_of_process.rs b/rust/build/out_of_process.rs index b8cd3acc3f..c0a9dd3050 100644 --- a/rust/build/out_of_process.rs +++ b/rust/build/out_of_process.rs @@ -13,20 +13,20 @@ pub(crate) fn main() { println!("cargo::rustc-check-cfg=cfg(has_extracted_cli)"); println!("cargo:rerun-if-changed=cli-version.txt"); - // Only declare the lockfile rerun when the lockfile actually exists. + // Only declare the package metadata rerun when it actually exists. // Cargo treats `rerun-if-changed` for a missing path as "always rerun" // — so unconditionally declaring this on consumers without a sibling // `nodejs/` (vendored slots, published crates) would force build.rs // to re-run on every `cargo build` even when nothing has changed. - // The lockfile path is only the source-of-truth in this repo's + // The package file is only the source-of-truth in this repo's // contributor builds; everywhere else `cli-version.txt` is canonical. let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); - let lockfile = Path::new(&manifest_dir) + let package_json = Path::new(&manifest_dir) .join("..") .join("nodejs") - .join("package-lock.json"); - if lockfile.is_file() { - println!("cargo:rerun-if-changed={}", lockfile.display()); + .join("package.json"); + if package_json.is_file() { + println!("cargo:rerun-if-changed={}", package_json.display()); } // Hard opt-out: disable the entire download / bundle / cache mechanism @@ -61,15 +61,9 @@ pub(crate) fn main() { // Resolve version + per-asset SHA-256 from one of two sources, in order: // 1. `cli-version.txt` snapshot at the crate root (published-crate - // consumer; generated by the publish workflow). Combined format: - // `version=X` line + per-asset hash lines. Committing the hashes - // makes the publish workflow the trust boundary — an attacker who - // later re-points the release tag can't silently poison consumer - // builds. - // 2. Sibling `../nodejs/package-lock.json` (contributor build inside - // the github/copilot-sdk repo; live SHA256SUMS.txt fetch). Matches - // the .NET `_GetCopilotCliVersion` MSBuild target and the Go - // `cmd/bundler` tool. + // consumer; generated by the publish workflow from SHA256SUMS.txt). + // 2. Sibling `../nodejs/package.json` plus the release SHA256SUMS.txt + // (contributor build inside the github/copilot-sdk repo). let (version, expected_hash) = resolve_version_and_hash(platform.asset_name); // Bake the version into the crate regardless of mode. This is the @@ -194,15 +188,14 @@ fn resolve_version_and_hash(asset_name: &str) -> (String, String) { .unwrap_or_else(|e| panic!("invalid {}: {e}", snapshot.display())); } - // 2. Lockfile fallback (contributor build inside github/copilot-sdk) — - // read version, fetch live SHA256SUMS. - let lockfile = Path::new(&manifest_dir) + // 2. Package version plus release checksums (contributor build). + let package_json = Path::new(&manifest_dir) .join("..") .join("nodejs") - .join("package-lock.json"); - if lockfile.is_file() { - let version = read_version_from_package_lock(&lockfile); - let hash = fetch_live_sha256(&version, asset_name); + .join("package.json"); + if package_json.is_file() { + let version = read_version_from_package_json(&package_json); + let hash = fetch_release_hash(&version, asset_name); return (version, hash); } @@ -212,9 +205,9 @@ fn resolve_version_and_hash(asset_name: &str) -> (String, String) { - {} (missing)\n\ - {} (missing)\n\ In a published crate or vendored slot, `cli-version.txt` should be present.\n\ - Inside the github/copilot-sdk repo, `../nodejs/package-lock.json` is the source.", + Inside the github/copilot-sdk repo, `../nodejs/package.json` is the version source.", snapshot.display(), - lockfile.display(), + package_json.display(), ); } @@ -247,39 +240,33 @@ fn parse_snapshot(contents: &str, asset_name: &str) -> Result<(String, String), Ok((version, hash)) } -/// Read the `@github/copilot` version from `nodejs/package-lock.json`. -fn read_version_from_package_lock(path: &Path) -> String { +fn read_version_from_package_json(path: &Path) -> String { let contents = std::fs::read_to_string(path) .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())); - // Minimal JSON walk: find `"node_modules/@github/copilot"` object and - // its `"version"` field. Full JSON parsing keeps build.rs dep-light by - // using a regex; the file is generated by npm and we're matching an - // exact key path. - let key = "\"node_modules/@github/copilot\""; - let key_pos = contents - .find(key) - .unwrap_or_else(|| panic!("{} does not contain {key}", path.display())); - let after_key = &contents[key_pos + key.len()..]; - let version_key = "\"version\""; - let v_pos = after_key - .find(version_key) - .unwrap_or_else(|| panic!("no `version` field found near {key} in {}", path.display())); - let after_v = &after_key[v_pos + version_key.len()..]; - let q1 = after_v.find('"').expect("malformed version"); - let after_q1 = &after_v[q1 + 1..]; - let q2 = after_q1.find('"').expect("malformed version"); - after_q1[..q2].to_string() + let package_json: serde_json::Value = serde_json::from_str(&contents) + .unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display())); + package_json["copilotCliVersion"] + .as_str() + .unwrap_or_else(|| panic!("copilotCliVersion is missing in {}", path.display())) + .to_string() } -/// Fetch the live `SHA256SUMS.txt` for the given version from GitHub Releases -/// and pluck out the entry for `asset_name`. -fn fetch_live_sha256(version: &str, asset_name: &str) -> String { - let base_url = format!("https://github.com/github/copilot-cli/releases/download/v{version}"); - let checksums_url = format!("{base_url}/SHA256SUMS.txt"); - let checksums = download_with_retry(&checksums_url); - let checksums_text = - std::str::from_utf8(&checksums).expect("checksums file is not valid UTF-8"); - find_sha256_for_asset(checksums_text, asset_name) +fn fetch_release_hash(version: &str, asset_name: &str) -> String { + let url = + format!("https://github.com/github/copilot-cli/releases/download/v{version}/SHA256SUMS.txt"); + let checksums = download_with_retry(&url); + let checksums = + std::str::from_utf8(&checksums).expect("SHA256SUMS.txt is not valid UTF-8"); + find_sha256_for_asset(checksums, asset_name) +} + +fn find_sha256_for_asset(sums: &str, asset_name: &str) -> String { + sums.lines() + .find_map(|line| { + let (hash, name) = line.split_once(char::is_whitespace)?; + (name.trim_start().trim_start_matches('*') == asset_name).then(|| hash.to_string()) + }) + .unwrap_or_else(|| panic!("SHA256SUMS.txt does not contain {asset_name}")) } #[derive(Clone, Copy)] @@ -641,18 +628,6 @@ fn try_download(url: &str) -> Result, DownloadError> { } } -fn find_sha256_for_asset(sums: &str, asset_name: &str) -> String { - for line in sums.lines() { - // Format: " " (two spaces) - if let Some((hash, name)) = line.split_once(" ") - && name.trim() == asset_name - { - return hash.trim().to_string(); - } - } - panic!("SHA256SUMS.txt does not contain an entry for {asset_name}"); -} - fn sha256(data: &[u8]) -> [u8; 32] { let mut hasher = sha2::Sha256::new(); hasher.update(data); diff --git a/rust/scripts/snapshot-bundled-cli-version.sh b/rust/scripts/snapshot-bundled-cli-version.sh index 7f78d529b0..08b19ebc2b 100755 --- a/rust/scripts/snapshot-bundled-cli-version.sh +++ b/rust/scripts/snapshot-bundled-cli-version.sh @@ -5,10 +5,9 @@ # how .NET's _GenerateVersionProps BeforeTargets="Pack" target writes # GitHub.Copilot.SDK.props before NuGet packing. # -# Inputs: -# - ../nodejs/package-lock.json (sibling) - source of the pinned version. -# - https://github.com/github/copilot-cli/releases/v{version}/SHA256SUMS.txt - -# authoritative per-platform hashes. +# Input: +# - ../nodejs/package.json - pinned release version. +# - The release's SHA256SUMS.txt - per-platform hashes. # # Output: # - cli-version.txt (in the rust crate root). Gitignored. @@ -18,23 +17,21 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RUST_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${RUST_DIR}/.." && pwd)" -LOCKFILE="${REPO_ROOT}/nodejs/package-lock.json" +PACKAGE_FILE="${REPO_ROOT}/nodejs/package.json" OUTPUT="${RUST_DIR}/cli-version.txt" -if [[ ! -f "${LOCKFILE}" ]]; then - echo "error: ${LOCKFILE} not found" >&2 +if [[ ! -f "${PACKAGE_FILE}" ]]; then + echo "error: ${PACKAGE_FILE} not found" >&2 exit 1 fi -VERSION="$(node -e "console.log(require('${LOCKFILE}').packages['node_modules/@github/copilot'].version)")" +VERSION="$(node -e "console.log(require('${PACKAGE_FILE}').copilotCliVersion)")" if [[ -z "${VERSION}" ]]; then - echo "error: could not read @github/copilot version from ${LOCKFILE}" >&2 + echo "error: could not read copilotCliVersion from ${PACKAGE_FILE}" >&2 exit 1 fi - CHECKSUMS_URL="https://github.com/github/copilot-cli/releases/download/v${VERSION}/SHA256SUMS.txt" -echo "Fetching ${CHECKSUMS_URL}" -SHA256SUMS="$(curl -fsSL --retry 3 --retry-delay 2 "${CHECKSUMS_URL}")" +SHA256SUMS="$(curl --fail --silent --show-error --location --retry 3 "${CHECKSUMS_URL}")" ASSETS=( "copilot-darwin-arm64.tar.gz" @@ -45,23 +42,22 @@ ASSETS=( "copilot-win32-x64.zip" ) -declare -A HASHES -for asset in "${ASSETS[@]}"; do - hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v a="${asset}" '$2 == a { print $1 }')" - if [[ -z "${hash}" ]]; then - echo "error: SHA256SUMS.txt missing entry for ${asset}" >&2 - exit 1 - fi - HASHES[$asset]="${hash}" -done - +TEMP_OUTPUT="${OUTPUT}.tmp.$$" +trap 'rm -f "${TEMP_OUTPUT}"' EXIT { echo "# Auto-generated by rust/scripts/snapshot-bundled-cli-version.sh" echo "# Do not edit. Regenerated by the publish workflow on every release." echo "version=${VERSION}" for asset in "${ASSETS[@]}"; do - echo "${asset}=${HASHES[$asset]}" + hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v asset="${asset}" '$2 == asset || $2 == "*" asset { print $1; exit }')" + if [[ -z "${hash}" ]]; then + echo "error: SHA256SUMS.txt does not contain ${asset}" >&2 + exit 1 + fi + echo "${asset}=${hash}" done -} > "${OUTPUT}" +} > "${TEMP_OUTPUT}" +mv "${TEMP_OUTPUT}" "${OUTPUT}" +trap - EXIT echo "Wrote ${OUTPUT} (version=${VERSION}, ${#ASSETS[@]} hashes)" \ No newline at end of file diff --git a/rust/scripts/snapshot-bundled-in-process-version.sh b/rust/scripts/snapshot-bundled-in-process-version.sh index 8743f9d17a..9fe2298c78 100755 --- a/rust/scripts/snapshot-bundled-in-process-version.sh +++ b/rust/scripts/snapshot-bundled-in-process-version.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Snapshot the Copilot CLI version + per-platform npm integrity values for the +# Snapshot the Copilot CLI version + per-platform release hashes for the # rust crate's bundled-in-process build path. set -euo pipefail @@ -8,19 +8,21 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RUST_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${RUST_DIR}/.." && pwd)" -LOCKFILE="${REPO_ROOT}/nodejs/package-lock.json" +PACKAGE_FILE="${REPO_ROOT}/nodejs/package.json" OUTPUT="${RUST_DIR}/cli-version-in-process.txt" -if [[ ! -f "${LOCKFILE}" ]]; then - echo "error: ${LOCKFILE} not found" >&2 +if [[ ! -f "${PACKAGE_FILE}" ]]; then + echo "error: ${PACKAGE_FILE} not found" >&2 exit 1 fi -VERSION="$(node -e "console.log(require('${LOCKFILE}').packages['node_modules/@github/copilot'].version)")" +VERSION="$(node -e "console.log(require('${PACKAGE_FILE}').copilotCliVersion)")" if [[ -z "${VERSION}" ]]; then - echo "error: could not read @github/copilot version from ${LOCKFILE}" >&2 + echo "error: could not read copilotCliVersion from ${PACKAGE_FILE}" >&2 exit 1 fi +CHECKSUMS_URL="https://github.com/github/copilot-cli/releases/download/v${VERSION}/SHA256SUMS.txt" +SHA256SUMS="$(curl --fail --silent --show-error --location --retry 3 "${CHECKSUMS_URL}")" PACKAGES=( "copilot-darwin-arm64" @@ -33,23 +35,24 @@ PACKAGES=( "copilot-win32-x64" ) -declare -A INTEGRITIES -for package in "${PACKAGES[@]}"; do - integrity="$(node -e "console.log(require('${LOCKFILE}').packages['node_modules/@github/${package}'].integrity)")" - if [[ -z "${integrity}" ]]; then - echo "error: package-lock.json missing integrity for @github/${package}" >&2 - exit 1 - fi - INTEGRITIES[$package]="${integrity}" -done - +TEMP_OUTPUT="${OUTPUT}.tmp.$$" +trap 'rm -f "${TEMP_OUTPUT}"' EXIT { echo "# Auto-generated by rust/scripts/snapshot-bundled-in-process-version.sh" echo "# Do not edit. Regenerated by the publish workflow on every release." echo "version=${VERSION}" for package in "${PACKAGES[@]}"; do - echo "${package}=${INTEGRITIES[$package]}" + platform="${package#copilot-}" + asset="github-copilot-${VERSION}-${platform}.tgz" + hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v asset="${asset}" '$2 == asset || $2 == "*" asset { print $1; exit }')" + if [[ -z "${hash}" ]]; then + echo "error: SHA256SUMS.txt does not contain ${asset}" >&2 + exit 1 + fi + echo "${package}=${hash}" done -} > "${OUTPUT}" +} > "${TEMP_OUTPUT}" +mv "${TEMP_OUTPUT}" "${OUTPUT}" +trap - EXIT -echo "Wrote ${OUTPUT} (version=${VERSION}, ${#PACKAGES[@]} integrity values)" +echo "Wrote ${OUTPUT} (version=${VERSION}, ${#PACKAGES[@]} hashes)" diff --git a/rust/tests/cli_resolution_test.rs b/rust/tests/cli_resolution_test.rs index 75773a0c0d..847ac7a4d0 100644 --- a/rust/tests/cli_resolution_test.rs +++ b/rust/tests/cli_resolution_test.rs @@ -199,16 +199,17 @@ async fn extract_dir_runtime_override_is_honored() { /// Build-time version pins, when present, must match the selected bundling /// implementation's checksum format. -/// When absent, build.rs falls through to `../nodejs/package-lock.json` — +/// When absent, build.rs falls through to `../nodejs/package.json` and +/// the release's `SHA256SUMS.txt` — /// both are accepted, this test only checks the pin file's format if it's /// there. #[test] fn pin_file_when_present_is_well_formed() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let (filename, value_prefix) = if cfg!(feature = "bundled-in-process") { - ("cli-version-in-process.txt", Some("sha512-")) + let (filename, expected_package_count) = if cfg!(feature = "bundled-in-process") { + ("cli-version-in-process.txt", 8) } else { - ("cli-version.txt", None) + ("cli-version.txt", 6) }; let pin = PathBuf::from(manifest_dir).join(filename); if !pin.is_file() { @@ -230,27 +231,20 @@ fn pin_file_when_present_is_well_formed() { if key.trim() == "version" { saw_version = true; } else { - if let Some(prefix) = value_prefix { - assert!( - value.trim().starts_with(prefix), - "invalid npm integrity for key {key:?}" - ); - } else { - assert_eq!( - value.trim().len(), - 64, - "invalid SHA-256 hash for key {key:?}" - ); - assert!( - value.trim().bytes().all(|byte| byte.is_ascii_hexdigit()), - "invalid SHA-256 hash for key {key:?}" - ); - } + assert_eq!( + value.trim().len(), + 64, + "invalid SHA-256 hash for key {key:?}" + ); + assert!( + value.trim().bytes().all(|byte| byte.is_ascii_hexdigit()), + "invalid SHA-256 hash for key {key:?}" + ); package_count += 1; } } assert!(saw_version, "{filename} missing `version=` line"); - assert_eq!(package_count, 6); + assert_eq!(package_count, expected_package_count); } /// With `bundled-cli` on AND a supported target, `install_bundled_cli` diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index 42accf2ec7..0b7bbae7a7 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -674,7 +674,7 @@ impl E2eContext { impl SharedE2eState { async fn prepare_test(&mut self, category: &str, snapshot_name: &str) -> std::io::Result<()> { self.cleanup_sessions().await?; - clear_directory_contents(self.context.work_dir())?; + clear_directory_contents(self.context.work_dir()).await?; self.context.configure(category, snapshot_name)?; self.context.set_default_copilot_user(); Ok(()) @@ -682,7 +682,7 @@ impl SharedE2eState { async fn cleanup_after_test(&mut self) -> std::io::Result<()> { self.cleanup_sessions().await?; - clear_directory_contents(self.context.work_dir()) + clear_directory_contents(self.context.work_dir()).await } async fn cleanup_sessions(&self) -> std::io::Result<()> { @@ -782,19 +782,36 @@ fn is_filtered_test_run() -> bool { }) } -fn clear_directory_contents(directory: &Path) -> std::io::Result<()> { +async fn clear_directory_contents(directory: &Path) -> std::io::Result<()> { for entry in std::fs::read_dir(directory)? { let entry = entry?; let path = entry.path(); - if entry.file_type()?.is_dir() { - std::fs::remove_dir_all(path)?; - } else { - std::fs::remove_file(path)?; + let is_directory = entry.file_type()?.is_dir(); + + for attempt in 1..=20 { + let result = if is_directory { + std::fs::remove_dir_all(&path) + } else { + std::fs::remove_file(&path) + }; + + match result { + Ok(()) => break, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => break, + Err(error) if is_transient_windows_file_lock(&error) && attempt < 20 => { + tokio::time::sleep(Duration::from_millis(50)).await; + } + Err(error) => return Err(error), + } } } Ok(()) } +fn is_transient_windows_file_lock(error: &std::io::Error) -> bool { + cfg!(windows) && matches!(error.raw_os_error(), Some(5 | 32 | 145)) +} + impl Drop for E2eContext { fn drop(&mut self) { if let Some(mut proxy) = self.proxy.take() { @@ -1175,29 +1192,23 @@ fn cli_path(repo_root: &Path) -> std::io::Result { } } - // 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. - let github_dir = repo_root - .join("nodejs") - .join("node_modules") - .join("@github"); - if let Ok(entries) = std::fs::read_dir(&github_dir) { - for entry in entries.flatten() { - if entry.file_name().to_string_lossy().starts_with("copilot-") { - let candidate = entry.path().join("index.js"); - if candidate.exists() { - return Ok(candidate); - } - } + let npm = if cfg!(windows) { "npm.cmd" } else { "npm" }; + let output = std::process::Command::new(npm) + .args(["run", "--silent", "prepare:runtime", "--", "--print-path"]) + .current_dir(repo_root.join("nodejs")) + .output()?; + if output.status.success() { + let path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim()); + if path.is_file() { + return Ok(path); } } Err(std::io::Error::new( std::io::ErrorKind::NotFound, format!( - "CLI not found under {}; run npm install in nodejs first", - github_dir.display() + "failed to prepare the pinned Copilot CLI: {}", + String::from_utf8_lossy(&output.stderr).trim() ), )) } diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 590213d72b..bad31a7675 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -12,6 +12,8 @@ import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; +import { COPILOT_CLI_VERSION } from "../../nodejs/src/cliVersion.js"; +import { ensureCopilotPackage } from "../../nodejs/scripts/releaseArtifacts.js"; export const execFileAsync = promisify(execFile); @@ -45,59 +47,23 @@ export type SchemaWithSharedDefinitions = T }; // ── Schema paths ──────────────────────────────────────────────────────────── -const SDK_NODE_MODULES = path.join(REPO_ROOT, "nodejs/node_modules"); - /** - * Resolve a JSON schema shipped by the `@github/copilot` CLI package. - * - * The CLI package layout changed in 1.0.64-1: the umbrella `@github/copilot` - * package became a thin loader and its bundled assets (including the JSON - * schemas) moved into the platform-specific packages installed as optional - * dependencies, e.g. `@github/copilot-linux-x64` or `@github/copilot-win32-x64`. - * - * To support both layouts we look in the umbrella package first (older - * versions) and then in whichever platform package was installed for the - * current host. + * Resolve a JSON schema from the pinned Copilot CLI GitHub Release. */ -async function resolveCopilotSchemaPath(nodeModulesDir: string, fileName: string): Promise { - const candidates = [path.join(nodeModulesDir, "@github/copilot/schemas", fileName)]; - - const githubScopeDir = path.join(nodeModulesDir, "@github"); - try { - for (const entry of await fs.readdir(githubScopeDir)) { - if (entry.startsWith("copilot-")) { - candidates.push(path.join(githubScopeDir, entry, "schemas", fileName)); - } - } - } catch (err) { - const code = (err as NodeJS.ErrnoException).code; - if (code !== "ENOENT" && code !== "ENOTDIR") { - throw err; - } - // @github scope directory may not exist yet; fall through to the error below. - } - - for (const candidate of candidates) { - try { - await fs.access(candidate); - return candidate; - } catch { - // Try the next candidate. - } - } - - throw new Error( - `${fileName} not found under ${githubScopeDir}. Run 'npm ci' in nodejs/ first.` - ); +async function resolveCopilotSchemaPath(fileName: string): Promise { + const packageRoot = await ensureCopilotPackage(COPILOT_CLI_VERSION); + const schemaPath = path.join(packageRoot, "schemas", fileName); + await fs.access(schemaPath); + return schemaPath; } export async function getSessionEventsSchemaPath(): Promise { - return resolveCopilotSchemaPath(SDK_NODE_MODULES, "session-events.schema.json"); + return resolveCopilotSchemaPath("session-events.schema.json"); } export async function getApiSchemaPath(cliArg?: string): Promise { if (cliArg) return cliArg; - return resolveCopilotSchemaPath(SDK_NODE_MODULES, "api.schema.json"); + return resolveCopilotSchemaPath("api.schema.json"); } // ── Brand casing normalization ────────────────────────────────────────────── diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 288ec1db3c..2488aa2c01 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.83-3", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -26,7 +25,7 @@ }, "node_modules/@emnapi/core": { "version": "1.10.0", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "integrity": "sha1-OAzMjyQS6iLR2XLff47iOjucdGc=", "dev": true, "license": "MIT", "optional": true, @@ -37,7 +36,7 @@ }, "node_modules/@emnapi/runtime": { "version": "1.10.0", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "integrity": "sha1-SyYMDTU0IE6YxhELjbGph9JuyHw=", "dev": true, "license": "MIT", "optional": true, @@ -47,7 +46,7 @@ }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "integrity": "sha1-KP7SGhuhznl8RKBwq8lNQvOuhUg=", "dev": true, "license": "MIT", "optional": true, @@ -57,7 +56,7 @@ }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "integrity": "sha1-egGo0uwvuy2seK2tCbD6eB5Agr4=", "cpu": [ "ppc64" ], @@ -73,7 +72,7 @@ }, "node_modules/@esbuild/android-arm": { "version": "0.28.1", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "integrity": "sha1-cEvSl95tdi3lTqu+r79V9nVqvi8=", "cpu": [ "arm" ], @@ -89,7 +88,7 @@ }, "node_modules/@esbuild/android-arm64": { "version": "0.28.1", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "integrity": "sha1-tUCifRTkr9BYSWpNvsTT9BTbEQo=", "cpu": [ "arm64" ], @@ -105,7 +104,7 @@ }, "node_modules/@esbuild/android-x64": { "version": "0.28.1", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "integrity": "sha1-0csWbTSw+/D+irRgpVlPJKN4cB4=", "cpu": [ "x64" ], @@ -121,7 +120,6 @@ }, "node_modules/@esbuild/darwin-arm64": { "version": "0.28.1", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -137,7 +135,7 @@ }, "node_modules/@esbuild/darwin-x64": { "version": "0.28.1", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "integrity": "sha1-ZVVqQyoeTXIDLYIYwZMvzKGkl3I=", "cpu": [ "x64" ], @@ -153,7 +151,7 @@ }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.28.1", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "integrity": "sha1-LmHgWS+QMNfj2uGO4l68U1kYrvY=", "cpu": [ "arm64" ], @@ -169,7 +167,7 @@ }, "node_modules/@esbuild/freebsd-x64": { "version": "0.28.1", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "integrity": "sha1-yV7CiZWe+AecTcqBeh4sS+Zrm9M=", "cpu": [ "x64" ], @@ -185,7 +183,7 @@ }, "node_modules/@esbuild/linux-arm": { "version": "0.28.1", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "integrity": "sha1-wJoPZ5F1kqwN6JKpvk04FN69Kmw=", "cpu": [ "arm" ], @@ -201,7 +199,7 @@ }, "node_modules/@esbuild/linux-arm64": { "version": "0.28.1", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "integrity": "sha1-QLIhdd2gYYLz7oFBGGxf8wTEpxc=", "cpu": [ "arm64" ], @@ -217,7 +215,7 @@ }, "node_modules/@esbuild/linux-ia32": { "version": "0.28.1", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "integrity": "sha1-pYD5xnZ5eDOJHlGfx6EzfIr9jbM=", "cpu": [ "ia32" ], @@ -233,7 +231,7 @@ }, "node_modules/@esbuild/linux-loong64": { "version": "0.28.1", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "integrity": "sha1-RkUs8yHcf56Rwvp4Cla7Vuec1os=", "cpu": [ "loong64" ], @@ -249,7 +247,7 @@ }, "node_modules/@esbuild/linux-mips64el": { "version": "0.28.1", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "integrity": "sha1-QhGzGE3WYI9T3LIuOfXTTuCIUsg=", "cpu": [ "mips64el" ], @@ -265,7 +263,7 @@ }, "node_modules/@esbuild/linux-ppc64": { "version": "0.28.1", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "integrity": "sha1-aXhXwqYcubC2u2ZS5AwdxeHKjl0=", "cpu": [ "ppc64" ], @@ -281,7 +279,7 @@ }, "node_modules/@esbuild/linux-riscv64": { "version": "0.28.1", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "integrity": "sha1-0ZKUPrFGpArExkl9DPe+NbmGvwg=", "cpu": [ "riscv64" ], @@ -297,7 +295,7 @@ }, "node_modules/@esbuild/linux-s390x": { "version": "0.28.1", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "integrity": "sha1-rOoDVtoODrwI+Xz3ucLkAeHmSNw=", "cpu": [ "s390x" ], @@ -313,7 +311,7 @@ }, "node_modules/@esbuild/linux-x64": { "version": "0.28.1", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "integrity": "sha1-bww84MtkxTS3DExF7LLBbTTjXf0=", "cpu": [ "x64" ], @@ -329,7 +327,7 @@ }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.28.1", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "integrity": "sha1-i813B3oNzjN4tXT+2ybSolO3PTY=", "cpu": [ "arm64" ], @@ -345,7 +343,7 @@ }, "node_modules/@esbuild/netbsd-x64": { "version": "0.28.1", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "integrity": "sha1-5/sqAemcgwyU5mI82f77TI+1g0c=", "cpu": [ "x64" ], @@ -361,7 +359,7 @@ }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.28.1", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "integrity": "sha1-xSkJNy24uG4sVeBaiUADO1Zgo7I=", "cpu": [ "arm64" ], @@ -377,7 +375,7 @@ }, "node_modules/@esbuild/openbsd-x64": { "version": "0.28.1", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "integrity": "sha1-xCe5vlpkwmL/mn63C1+7qt9EbGw=", "cpu": [ "x64" ], @@ -393,7 +391,7 @@ }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.28.1", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "integrity": "sha1-3JsUe6yi5sSzyFVxdB70hgpIkJc=", "cpu": [ "arm64" ], @@ -409,7 +407,7 @@ }, "node_modules/@esbuild/sunos-x64": { "version": "0.28.1", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "integrity": "sha1-zoZtEt8TwV5MmfBzo9Rm9uBkmzo=", "cpu": [ "x64" ], @@ -425,7 +423,7 @@ }, "node_modules/@esbuild/win32-arm64": { "version": "0.28.1", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "integrity": "sha1-dGjjaS0B1inVlB5dg4F7uA+eObQ=", "cpu": [ "arm64" ], @@ -441,7 +439,7 @@ }, "node_modules/@esbuild/win32-ia32": { "version": "0.28.1", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "integrity": "sha1-pbwAY/sryrbQ7WPyoVN5WLwmnsY=", "cpu": [ "ia32" ], @@ -457,7 +455,7 @@ }, "node_modules/@esbuild/win32-x64": { "version": "0.28.1", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "integrity": "sha1-EAZO5E9DR7kMmgK0Rrv4CpFjKxI=", "cpu": [ "x64" ], @@ -471,159 +469,8 @@ "node": ">=18" } }, - "node_modules/@github/copilot": { - "version": "1.0.83-3", - "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==", - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "detect-libc": "^2.1.2" - }, - "bin": { - "copilot": "npm-loader.js" - }, - "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-3", - "@github/copilot-darwin-x64": "1.0.83-3", - "@github/copilot-linux-arm64": "1.0.83-3", - "@github/copilot-linux-x64": "1.0.83-3", - "@github/copilot-linuxmusl-arm64": "1.0.83-3", - "@github/copilot-linuxmusl-x64": "1.0.83-3", - "@github/copilot-win32-arm64": "1.0.83-3", - "@github/copilot-win32-x64": "1.0.83-3" - } - }, - "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-3", - "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-arm64": "copilot" - } - }, - "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-3", - "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-x64": "copilot" - } - }, - "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-3", - "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-3", - "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-x64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-3", - "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-3", - "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-x64": "copilot" - } - }, - "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-3", - "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-arm64": "copilot.exe" - } - }, - "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-3", - "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-x64": "copilot.exe" - } - }, "node_modules/@hono/node-server": { "version": "1.19.14", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "dev": true, "license": "MIT", "engines": { @@ -635,13 +482,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.26.0", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "dev": true, "license": "MIT", "dependencies": { @@ -680,26 +525,28 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.2.3", + "integrity": "sha1-l+PUXXQk3F2h1OMvO/OykvbBtEw=", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@oxc-project/types": { "version": "0.133.0", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -708,7 +555,7 @@ }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "integrity": "sha1-VM6Pg4IhP0oxSgwve6g/gf/q5ZI=", "cpu": [ "arm64" ], @@ -724,7 +571,6 @@ }, "node_modules/@rolldown/binding-darwin-arm64": { "version": "1.0.3", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -740,7 +586,7 @@ }, "node_modules/@rolldown/binding-darwin-x64": { "version": "1.0.3", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "integrity": "sha1-U/V94fWZ7PHbE4I8/IjBj7gJVK0=", "cpu": [ "x64" ], @@ -756,7 +602,7 @@ }, "node_modules/@rolldown/binding-freebsd-x64": { "version": "1.0.3", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "integrity": "sha1-bz/dobeuqsnSaKUmgEtPuW5ONfE=", "cpu": [ "x64" ], @@ -772,7 +618,7 @@ }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { "version": "1.0.3", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "integrity": "sha1-2HpFS/WFzJZ2hJN36R1uN1KXMm8=", "cpu": [ "arm" ], @@ -788,7 +634,7 @@ }, "node_modules/@rolldown/binding-linux-arm64-gnu": { "version": "1.0.3", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "integrity": "sha1-QZ/Wv2Es80jxBSjLzZTrq5YH2NE=", "cpu": [ "arm64" ], @@ -804,7 +650,7 @@ }, "node_modules/@rolldown/binding-linux-arm64-musl": { "version": "1.0.3", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "integrity": "sha1-/MaRhpa7doRId+HkkwoY/Q03QGk=", "cpu": [ "arm64" ], @@ -820,7 +666,7 @@ }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { "version": "1.0.3", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "integrity": "sha1-Mq7LfI2uXU8qjN5XoFjshpkVQvg=", "cpu": [ "ppc64" ], @@ -836,7 +682,7 @@ }, "node_modules/@rolldown/binding-linux-s390x-gnu": { "version": "1.0.3", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "integrity": "sha1-vtk0bqgea7i5PPEfXYi3fbiQt2M=", "cpu": [ "s390x" ], @@ -852,7 +698,7 @@ }, "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.0.3", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "integrity": "sha1-ZMLSb3Xf/ZtaH5dVegCudyUMjLc=", "cpu": [ "x64" ], @@ -868,7 +714,7 @@ }, "node_modules/@rolldown/binding-linux-x64-musl": { "version": "1.0.3", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "integrity": "sha1-WkUTLopHZZ7qrztUDClUqXyGD/M=", "cpu": [ "x64" ], @@ -884,7 +730,7 @@ }, "node_modules/@rolldown/binding-openharmony-arm64": { "version": "1.0.3", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "integrity": "sha1-KQUTBoxV6EnchFejKv7h17Csswk=", "cpu": [ "arm64" ], @@ -900,7 +746,7 @@ }, "node_modules/@rolldown/binding-wasm32-wasi": { "version": "1.0.3", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "integrity": "sha1-PZly2/GpU9PHr6pKDyDvKy458xs=", "cpu": [ "wasm32" ], @@ -918,7 +764,7 @@ }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.3", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "integrity": "sha1-oASrYHoW1vA7y1VXKP+IivdXc60=", "cpu": [ "arm64" ], @@ -934,7 +780,7 @@ }, "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.0.3", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "integrity": "sha1-4qJbNGkaHMihIJ195wkGMCbdDNs=", "cpu": [ "x64" ], @@ -950,19 +796,17 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "integrity": "sha1-AVy6np3UfOFNA9KoxdVHv7FpZl0=", "dev": true, "license": "MIT", "optional": true, @@ -972,7 +816,6 @@ }, "node_modules/@types/chai": { "version": "5.2.3", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -982,19 +825,16 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "25.3.3", - "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1003,7 +843,6 @@ }, "node_modules/@types/node-forge": { "version": "1.3.14", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1012,7 +851,6 @@ }, "node_modules/@vitest/expect": { "version": "4.1.8", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1029,7 +867,6 @@ }, "node_modules/@vitest/mocker": { "version": "4.1.8", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { @@ -1055,7 +892,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.1.8", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { @@ -1067,7 +903,6 @@ }, "node_modules/@vitest/runner": { "version": "4.1.8", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1080,7 +915,6 @@ }, "node_modules/@vitest/snapshot": { "version": "4.1.8", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1095,7 +929,6 @@ }, "node_modules/@vitest/spy": { "version": "4.1.8", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", "funding": { @@ -1104,7 +937,6 @@ }, "node_modules/@vitest/utils": { "version": "4.1.8", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -1118,7 +950,6 @@ }, "node_modules/accepts": { "version": "2.0.0", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", "dependencies": { @@ -1131,7 +962,6 @@ }, "node_modules/ajv": { "version": "8.18.0", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -1147,7 +977,6 @@ }, "node_modules/ajv-formats": { "version": "3.0.1", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1164,7 +993,6 @@ }, "node_modules/assertion-error": { "version": "2.0.1", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -1173,7 +1001,6 @@ }, "node_modules/body-parser": { "version": "2.2.2", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "license": "MIT", "dependencies": { @@ -1197,7 +1024,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", "engines": { @@ -1206,7 +1032,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1219,7 +1044,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -1235,7 +1059,6 @@ }, "node_modules/chai": { "version": "6.2.2", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -1244,7 +1067,6 @@ }, "node_modules/content-disposition": { "version": "1.0.1", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "dev": true, "license": "MIT", "engines": { @@ -1257,7 +1079,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", "engines": { @@ -1266,13 +1087,11 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -1281,7 +1100,6 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, "license": "MIT", "engines": { @@ -1290,7 +1108,6 @@ }, "node_modules/cors": { "version": "2.8.6", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", "dependencies": { @@ -1307,7 +1124,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -1321,7 +1137,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1338,7 +1153,6 @@ }, "node_modules/depd": { "version": "2.0.0", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", "engines": { @@ -1347,7 +1161,6 @@ }, "node_modules/detect-libc": { "version": "2.1.2", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1356,7 +1169,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -1370,13 +1182,11 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -1385,7 +1195,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -1394,7 +1203,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -1403,13 +1211,11 @@ }, "node_modules/es-module-lexer": { "version": "2.1.0", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -1421,7 +1227,6 @@ }, "node_modules/esbuild": { "version": "0.28.1", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1462,13 +1267,11 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, "license": "MIT" }, "node_modules/estree-walker": { "version": "3.0.3", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -1477,7 +1280,6 @@ }, "node_modules/etag": { "version": "1.8.1", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { @@ -1486,7 +1288,6 @@ }, "node_modules/eventsource": { "version": "3.0.7", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", "dependencies": { @@ -1498,7 +1299,6 @@ }, "node_modules/eventsource-parser": { "version": "3.0.6", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "dev": true, "license": "MIT", "engines": { @@ -1507,7 +1307,6 @@ }, "node_modules/expect-type": { "version": "1.3.0", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1516,7 +1315,6 @@ }, "node_modules/express": { "version": "5.2.1", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", "dependencies": { @@ -1559,7 +1357,6 @@ }, "node_modules/express-rate-limit": { "version": "8.5.2", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "dev": true, "license": "MIT", "dependencies": { @@ -1577,13 +1374,11 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.5", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { @@ -1599,7 +1394,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -1616,7 +1410,6 @@ }, "node_modules/finalhandler": { "version": "2.1.1", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { @@ -1637,7 +1430,6 @@ }, "node_modules/forwarded": { "version": "0.2.0", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", "engines": { @@ -1646,7 +1438,6 @@ }, "node_modules/fresh": { "version": "2.0.0", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", "engines": { @@ -1655,9 +1446,7 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -1669,7 +1458,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -1678,7 +1466,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1702,7 +1489,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -1715,7 +1501,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -1727,7 +1512,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -1739,7 +1523,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1751,7 +1534,6 @@ }, "node_modules/hono": { "version": "4.13.1", - "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", "dev": true, "license": "MIT", "engines": { @@ -1760,7 +1542,6 @@ }, "node_modules/http-errors": { "version": "2.0.1", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1780,7 +1561,6 @@ }, "node_modules/iconv-lite": { "version": "0.7.2", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -1796,13 +1576,11 @@ }, "node_modules/inherits": { "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC" }, "node_modules/ip-address": { "version": "10.4.0", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", "dev": true, "license": "MIT", "engines": { @@ -1811,7 +1589,6 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { @@ -1820,19 +1597,16 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true, "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/jose": { "version": "6.1.3", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", "dev": true, "license": "MIT", "funding": { @@ -1841,19 +1615,16 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/json-schema-typed": { "version": "8.0.2", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/lightningcss": { "version": "1.32.0", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -1882,7 +1653,7 @@ }, "node_modules/lightningcss-android-arm64": { "version": "1.32.0", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "integrity": "sha1-8DOIURbf79nG9UeHUj41FLYeGWg=", "cpu": [ "arm64" ], @@ -1902,7 +1673,6 @@ }, "node_modules/lightningcss-darwin-arm64": { "version": "1.32.0", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -1922,7 +1692,7 @@ }, "node_modules/lightningcss-darwin-x64": { "version": "1.32.0", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "integrity": "sha1-NfPpczLRMLnKGB4RtWje1q68bV4=", "cpu": [ "x64" ], @@ -1942,7 +1712,7 @@ }, "node_modules/lightningcss-freebsd-x64": { "version": "1.32.0", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "integrity": "sha1-l3enZHK2Ttb/lDQq1kx7r9eUpXU=", "cpu": [ "x64" ], @@ -1962,7 +1732,7 @@ }, "node_modules/lightningcss-linux-arm-gnueabihf": { "version": "1.32.0", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "integrity": "sha1-E65lLhq3O5E117faFy9mbEEK1T0=", "cpu": [ "arm" ], @@ -1982,7 +1752,7 @@ }, "node_modules/lightningcss-linux-arm64-gnu": { "version": "1.32.0", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "integrity": "sha1-QXhYeVqUWS9oASOhsfnaig4e8zU=", "cpu": [ "arm64" ], @@ -2002,7 +1772,7 @@ }, "node_modules/lightningcss-linux-arm64-musl": { "version": "1.32.0", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "integrity": "sha1-a+NmkugQtxgECAL9gJYjz/5zITM=", "cpu": [ "arm64" ], @@ -2022,7 +1792,7 @@ }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "integrity": "sha1-C3gDr06yHP043Tn+Kru1PH3QkfY=", "cpu": [ "x64" ], @@ -2042,7 +1812,7 @@ }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.32.0", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "integrity": "sha1-iNyLqGXd3bGsXvBLDxYYBEGMFjs=", "cpu": [ "x64" ], @@ -2062,7 +1832,7 @@ }, "node_modules/lightningcss-win32-arm64-msvc": { "version": "1.32.0", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "integrity": "sha1-TzC6P6XpJfW3n5RejMDRdsOxqzg=", "cpu": [ "arm64" ], @@ -2082,7 +1852,7 @@ }, "node_modules/lightningcss-win32-x64-msvc": { "version": "1.32.0", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "integrity": "sha1-FBqlYFZFBkkokCu0rwRfp9n0Igo=", "cpu": [ "x64" ], @@ -2102,7 +1872,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2111,7 +1880,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -2120,7 +1888,6 @@ }, "node_modules/media-typer": { "version": "1.1.0", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, "license": "MIT", "engines": { @@ -2129,7 +1896,6 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", "engines": { @@ -2141,7 +1907,6 @@ }, "node_modules/mime-db": { "version": "1.54.0", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { @@ -2150,7 +1915,6 @@ }, "node_modules/mime-types": { "version": "3.0.2", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { @@ -2166,13 +1930,11 @@ }, "node_modules/ms": { "version": "2.1.3", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.17", - "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "dev": true, "funding": [ { @@ -2190,7 +1952,6 @@ }, "node_modules/negotiator": { "version": "1.0.0", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -2199,7 +1960,6 @@ }, "node_modules/node-forge": { "version": "1.4.0", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -2208,7 +1968,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", "engines": { @@ -2217,7 +1976,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -2229,7 +1987,6 @@ }, "node_modules/obug": { "version": "2.1.1", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -2239,7 +1996,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -2251,7 +2007,6 @@ }, "node_modules/once": { "version": "1.4.0", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -2260,7 +2015,6 @@ }, "node_modules/openai": { "version": "6.17.0", - "integrity": "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2281,7 +2035,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", "engines": { @@ -2290,7 +2043,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -2299,7 +2051,6 @@ }, "node_modules/path-to-regexp": { "version": "8.4.2", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", "funding": { @@ -2309,19 +2060,16 @@ }, "node_modules/pathe": { "version": "2.0.3", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -2333,7 +2081,6 @@ }, "node_modules/pkce-challenge": { "version": "5.0.1", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "dev": true, "license": "MIT", "engines": { @@ -2342,7 +2089,6 @@ }, "node_modules/postcss": { "version": "8.5.25", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -2370,7 +2116,6 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "license": "MIT", "dependencies": { @@ -2383,7 +2128,6 @@ }, "node_modules/qs": { "version": "6.15.2", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2398,7 +2142,6 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", "engines": { @@ -2407,7 +2150,6 @@ }, "node_modules/raw-body": { "version": "3.0.2", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { @@ -2422,7 +2164,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { @@ -2431,7 +2172,6 @@ }, "node_modules/rolldown": { "version": "1.0.3", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { @@ -2464,7 +2204,6 @@ }, "node_modules/router": { "version": "2.2.0", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2480,13 +2219,11 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/send": { "version": "1.2.1", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2512,7 +2249,6 @@ }, "node_modules/serve-static": { "version": "2.2.1", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { @@ -2531,13 +2267,11 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true, "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -2549,7 +2283,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -2558,7 +2291,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { @@ -2577,7 +2309,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", "dependencies": { @@ -2593,7 +2324,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { @@ -2611,7 +2341,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { @@ -2630,13 +2359,11 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/source-map-js": { "version": "1.2.1", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2645,13 +2372,11 @@ }, "node_modules/stackback": { "version": "0.0.2", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -2660,19 +2385,16 @@ }, "node_modules/std-env": { "version": "4.1.0", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, "node_modules/tinybench": { "version": "2.9.0", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.0.2", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, "license": "MIT", "engines": { @@ -2681,7 +2403,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.17", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -2697,7 +2418,6 @@ }, "node_modules/tinyrainbow": { "version": "3.1.0", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -2706,7 +2426,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", "engines": { @@ -2715,14 +2434,13 @@ }, "node_modules/tslib": { "version": "2.8.1", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", "dev": true, "license": "0BSD", "optional": true }, "node_modules/tsx": { "version": "4.22.4", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { @@ -2740,7 +2458,6 @@ }, "node_modules/type-is": { "version": "2.0.1", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -2754,7 +2471,6 @@ }, "node_modules/typescript": { "version": "5.9.3", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2767,13 +2483,11 @@ }, "node_modules/undici-types": { "version": "7.18.2", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "license": "MIT", "engines": { @@ -2782,7 +2496,6 @@ }, "node_modules/vary": { "version": "1.1.2", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", "engines": { @@ -2791,7 +2504,6 @@ }, "node_modules/vite": { "version": "8.0.16", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { @@ -2868,7 +2580,6 @@ }, "node_modules/vitest": { "version": "4.1.8", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", "dependencies": { @@ -2957,7 +2668,6 @@ }, "node_modules/which": { "version": "2.0.2", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -2972,7 +2682,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -2988,13 +2697,11 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/yaml": { "version": "2.9.0", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { @@ -3009,7 +2716,6 @@ }, "node_modules/zod": { "version": "4.3.6", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "funding": { @@ -3018,7 +2724,6 @@ }, "node_modules/zod-to-json-schema": { "version": "3.25.1", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "dev": true, "license": "ISC", "peerDependencies": { diff --git a/test/harness/package.json b/test/harness/package.json index 7b04900614..9b37dfb9d0 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,6 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.83-3", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", From 9f177560acc1f7056d1b3e255856685df9d657fc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Sep 2026 12:34:57 +0000 Subject: [PATCH 36/60] docs: update version references to 1.0.13-preview.5 --- java/README.md | 8 ++++---- java/sdk/jbang-example.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java/README.md b/java/README.md index 356c1c5c1d..bb71d7ab86 100644 --- a/java/README.md +++ b/java/README.md @@ -36,14 +36,14 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. com.github copilot-sdk-java - 1.0.13-preview.4 + 1.0.13-preview.5 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.13-preview.4' +implementation 'com.github:copilot-sdk-java:1.0.13-preview.5' ``` #### Snapshot Builds @@ -62,7 +62,7 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.14-preview.4-SNAPSHOT + 1.0.14-preview.5-SNAPSHOT ``` @@ -71,7 +71,7 @@ Snapshot builds of the next development version are published to Maven Central S Replace `${copilot.sdk.version}` with the latest release from Maven Central. ```groovy -implementation 'com.github:copilot-sdk-java:1.0.14-preview.4-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.14-preview.5-SNAPSHOT' ``` ## In-process mode (experimental) diff --git a/java/sdk/jbang-example.java b/java/sdk/jbang-example.java index 0a1505f9c2..a223dd8d20 100644 --- a/java/sdk/jbang-example.java +++ b/java/sdk/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.13-preview.4 +//DEPS com.github:copilot-sdk-java:1.0.13-preview.5 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; From 6d80bbfb79f9b5d6e2fadbd04abcd8d524bafda8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Sep 2026 12:35:39 +0000 Subject: [PATCH 37/60] [maven-release-plugin] prepare release java/v1.0.13-preview.5 --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 8dff6c4ad6..d3826f77bb 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.4-SNAPSHOT + 1.0.13-preview.5 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.5 diff --git a/java/pom.xml b/java/pom.xml index a9e6836242..105f3630d8 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.4-SNAPSHOT + 1.0.13-preview.5 pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.5 diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index 4f6ae9771e..1238f9671d 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.4-SNAPSHOT + 1.0.13-preview.5 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.5 From a27af618465f3d40294f8710f210b07ff2c6e4fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Sep 2026 12:35:43 +0000 Subject: [PATCH 38/60] [maven-release-plugin] prepare for next development iteration --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index d3826f77bb..0abe03ad18 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.5 + 1.0.14-preview.5-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.5 + HEAD diff --git a/java/pom.xml b/java/pom.xml index 105f3630d8..285cb7bb9a 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.5 + 1.0.14-preview.5-SNAPSHOT pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.5 + HEAD diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index 1238f9671d..aa40b25189 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.5 + 1.0.14-preview.5-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.5 + HEAD From 54cd6738b7882156d24fcf8ea9fd4b86fc7625a2 Mon Sep 17 00:00:00 2001 From: sama Pyb Date: Thu, 3 Sep 2026 13:53:21 +0000 Subject: [PATCH 39/60] fix(go): select the host CLI package in E2E tests (#2163) * fix(go): select the host CLI package in E2E tests * ci: trigger pull request checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Pybsama <294532706+Pybsama@users.noreply.github.com> Co-authored-by: Steve Sanderson Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From d91189e0fb8bc015759cf432eba366979e69890c Mon Sep 17 00:00:00 2001 From: Rince Yuan Date: Thu, 3 Sep 2026 14:03:10 +0000 Subject: [PATCH 40/60] fix(rust): answer the request id when a handler panics (#2311) Requests are dispatched to their own spawned tasks, so a panic inside a handler is isolated by tokio and the request is simply never answered - the caller waits out its own timeout. Catch the unwind at the dispatch boundary and reply with a JSON-RPC internal error (-32603) instead; the panic payload is not exposed. Also corrects the stop_event_loop, Drop and lifecycle-test documentation, which still claimed in-flight handlers complete before the loop exits. That stopped being true when request dispatch moved to spawned tasks. Fixes #2053. Co-authored-by: Zhangyi Yuan --- rust/src/session.rs | 43 +++++++++++++++------ rust/tests/session_test.rs | 77 ++++++++++++++++++++++++++++++++------ 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/rust/src/session.rs b/rust/src/session.rs index 65c2aff7c4..509485e39e 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -1,14 +1,16 @@ use std::collections::HashMap; +use std::panic::AssertUnwindSafe; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{Duration, Instant}; +use futures_util::FutureExt; use parking_lot::Mutex as ParkingLotMutex; use serde_json::Value; use tokio::sync::oneshot; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; -use tracing::{Instrument, warn}; +use tracing::{Instrument, error, warn}; use crate::canvas::CanvasHandler; use crate::generated::api_types::{ @@ -323,10 +325,15 @@ impl Session { /// Stop the internal event loop. Called automatically on [`destroy`](Self::destroy). /// /// Cooperative: signals shutdown via the session's [`CancellationToken`] - /// and awaits the loop's natural exit rather than aborting the task. - /// Any in-flight handler (permission callback, tool call, elicitation - /// response) completes before the loop exits, so the CLI never sees a - /// half-handled request. See RFD-400 review finding #3. + /// and awaits the loop's natural exit rather than aborting the task, so + /// the loop always stops between iterations instead of at an arbitrary + /// await point. See RFD-400 review finding #3. + /// + /// Inbound requests are dispatched to their own spawned tasks, which this + /// call does not await. A handler (permission callback, tool call, + /// elicitation response) still running at teardown may therefore outlive + /// the loop, and its response can be lost if the connection closes first. + /// Await your own handler work before calling this if it must complete. pub async fn stop_event_loop(&self) { self.shutdown.cancel(); let handle = self.event_loop.lock().take(); @@ -653,11 +660,11 @@ impl Drop for Session { fn drop(&mut self) { // Cooperative shutdown: cancel the event loop's token to signal // exit between iterations. The loop will see the cancellation on - // its next select poll and break cleanly without interrupting an - // in-flight handler. We do NOT abort the JoinHandle — that would - // land at any await point in the loop body, potentially leaving - // the CLI with an unanswered request id. RFD-400 review finding - // #3. + // its next select poll and break cleanly. We do NOT abort the + // JoinHandle — that would land at any await point in the loop body, + // potentially leaving the CLI with an unanswered request id. + // RFD-400 review finding #3. Requests already dispatched to their + // own tasks are not tracked here and may outlive the session. // // The handle itself is left in `event_loop` to be reaped by the // tokio runtime when it next polls; we intentionally don't await @@ -1583,6 +1590,8 @@ fn spawn_event_loop( let canvas_handler = canvas_handler.clone(); let session_fs_provider = session_fs_provider.clone(); let bearer_token_providers = bearer_token_providers.clone(); + let request_id = request.id; + let method = request.method.clone(); tokio::spawn( async move { let ctx = RequestDispatchContext { @@ -1594,7 +1603,19 @@ fn spawn_event_loop( session_fs_provider: session_fs_provider.as_ref(), bearer_token_providers: &bearer_token_providers, }; - handle_request(&session_id, ctx, request).await; + let dispatch = handle_request(&session_id, ctx, request); + if AssertUnwindSafe(dispatch).catch_unwind().await.is_err() { + // Tokio isolates the panic to this task, so without a + // reply the CLI waits out its own timeout on this id. + error!(method = %method, "request handler panicked"); + let _ = send_error_response( + &client, + request_id, + error_codes::INTERNAL_ERROR, + "request handler panicked", + ) + .await; + } } .instrument(span), ); diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 9f86777ffc..778e13e71f 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -3544,10 +3544,10 @@ async fn send_and_wait_drop_clears_waiter() { } /// Cancel-safety regression: `Session::stop_event_loop` must NOT abort -/// the event-loop task mid-handler. An in-flight handler (here a slow -/// `userInput.request` callback) must run to completion before the loop -/// exits — the CLI receives the response on the wire before the session -/// tears down. +/// the event-loop task at an arbitrary await point. Requests are +/// dispatched to their own tasks, so a handler that is still running when +/// shutdown is signalled keeps going and its response still reaches the +/// wire rather than being lost mid-protocol. /// /// Closes RFD-400 review finding #3. #[tokio::test] @@ -3591,31 +3591,84 @@ async fn stop_event_loop_completes_in_flight_handler() { // Give the loop a moment to dispatch into the handler. tokio::time::sleep(Duration::from_millis(20)).await; - // Now request shutdown. The loop is parked in handle_request awaiting - // the slow handler. `notify_one()` buffers the signal until the loop - // re-enters its select, which can only happen after the handler - // returns and the response is sent on the wire. + // Now request shutdown while the spawned handler is still sleeping. let stop_handle = tokio::spawn({ let session = session.clone(); async move { session.stop_event_loop().await } }); - // Verify the handler's response lands on the wire BEFORE the loop - // exits — i.e. stop_event_loop did not abort mid-handler. + // The handler task is independent of the loop, so its response still + // lands on the wire instead of being lost to an aborted task. let response = timeout(Duration::from_secs(2), server.read_response()) .await .unwrap(); assert_eq!(response["id"], 900); assert_eq!(response["result"]["answer"], "completed"); - // stop_event_loop completes after the handler returns and the loop - // observes the buffered shutdown signal on its next select iteration. timeout(Duration::from_secs(2), stop_handle) .await .unwrap() .unwrap(); } +/// A panicking request handler must still answer its request id. Tokio +/// isolates the panic to the spawned handler task, so without an explicit +/// reply the caller would wait out its own timeout on a request that can +/// never complete. +#[tokio::test] +async fn panicking_request_handler_responds_with_internal_error() { + struct PanickingHandler; + #[async_trait] + impl UserInputHandler for PanickingHandler { + async fn handle( + &self, + _session_id: SessionId, + _question: String, + _choices: Option>, + _allow_freeform: Option, + ) -> Option { + panic!("handler blew up"); + } + } + + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_user_input_handler(Arc::new(PanickingHandler)) + }) + .await; + + server + .send_request( + 901, + "userInput.request", + serde_json::json!({ + "sessionId": server.session_id, + "question": "boom", + "choices": null, + "allowFreeform": true, + }), + ) + .await; + + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["id"], 901); + assert_eq!(response["error"]["code"], -32603); + assert!(response.get("result").is_none()); + + // The loop survives the panicking handler and keeps serving requests. + server + .send_request( + 902, + "unknown.method", + serde_json::json!({ "sessionId": server.session_id }), + ) + .await; + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["id"], 902); + assert_eq!(response["error"]["code"], -32601); + + session.stop_event_loop().await; +} + /// Cancel-safety regression: dropping a Session does NOT abort the event /// loop mid-handler. The loop sees the buffered shutdown signal on its /// next select iteration and exits cleanly. This is the Drop equivalent From 4c72ec1f9ec702c3fa1ff7bce57d93f214b67f7f Mon Sep 17 00:00:00 2001 From: green3sf <222944370+green3sf@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:13:50 +0800 Subject: [PATCH 41/60] fix(go): close failed session event loops (#2360) * fix(go): close failed session event loops * fix(go): also close event channel on failed ResumeSession ResumeSessionWithOptions pre-registers a session and starts its event loop before the RPC completes, the same eager-registration pattern CreateSession has. Every RPC-failure path called restoreReplacedSession (which correctly restores any session previously registered under this id, or deletes the map entry) but never closed the new, failed session's eventCh, leaking its processEvents goroutine per failed call - the same bug class this PR already fixes for CreateSession. Adds unit tests mirroring the existing CreateSession coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(go): add E2E regression test for session event-loop leak Real-CLI, no-mocks coverage (goroutine counting across 20 repeated failures) for both CreateSession and ResumeSession failure paths. Verified this test fails against the pre-fix code (goroutine count grows by exactly one per failed call) and passes with the fix applied. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(dotnet): close event channel on failed session create/resume CopilotSession has the identical eager-background-consumer pattern as Go: CreateSessionAsync/ResumeSessionAsync register the session and call StartProcessingEvents() (which starts ProcessEventsAsync() as a background task) before the RPC completes, but their catch blocks only called RemoveFromClient() - never completing _eventChannel - so the consumer task (and the object graph it implicitly keeps alive) runs forever on every failed call, expressed as a task/GC leak rather than a goroutine leak. Adds CopilotSession.CloseEventChannel()/Unregister() and wires both catch blocks (and DisposeAsync) through them. Adds a real-CLI E2E test (401 via proxy redirect for CreateSessionAsync, nonexistent-session-id for ResumeSessionAsync) using reflection to observe the session dictionary and event-channel completion state mid-flight, since .NET has no goroutine-count equivalent. Verified this test fails against the pre-fix code and passes with the fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Steve Sanderson Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 4 +- dotnet/src/Session.cs | 27 ++- .../test/E2E/SessionEventLoopLeakE2ETests.cs | 198 ++++++++++++++++ go/client.go | 38 +-- go/client_test.go | 219 ++++++++++++++++++ .../e2e/session_event_loop_leak_e2e_test.go | 102 ++++++++ go/session.go | 3 + 7 files changed, 573 insertions(+), 18 deletions(-) create mode 100644 dotnet/test/E2E/SessionEventLoopLeakE2ETests.cs create mode 100644 go/internal/e2e/session_event_loop_leak_e2e_test.go diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 9bd7a5fb54..2298edc0ce 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1356,7 +1356,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance } catch (Exception ex) { - session?.RemoveFromClient(); + session?.Unregister(); if (ex is not OperationCanceledException) { @@ -1561,7 +1561,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes } catch (Exception ex) { - session?.RemoveFromClient(); + session?.Unregister(); if (ex is not OperationCanceledException) { LoggingHelpers.LogTiming(_logger, LogLevel.Warning, ex, diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 5995abaaff..24450a7c7d 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -204,6 +204,31 @@ internal void RemoveFromClient() ((ICollection>)_parentClient._sessions).Remove(new(SessionId, this)); } + ///

+ /// Stops the session's event consumer () without + /// making an RPC. and + /// use this on error paths where a + /// locally registered session fails before it can be returned to the caller: + /// starts the consumer eagerly, and no caller + /// ever receives the failed session to dispose it. Safe to call more than once — + /// is idempotent. + /// + internal void CloseEventChannel() + { + _eventChannel.Writer.TryComplete(); + } + + /// + /// Removes the session from its parent client and stops its event consumer. + /// Used on session-creation/resume error paths where the session was registered + /// (and its event loop started) but never returned to the caller. + /// + internal void Unregister() + { + CloseEventChannel(); + RemoveFromClient(); + } + internal void SetGitHubTokenProviderRegistration(string registrationId) { _gitHubTokenProviderRegistrationId = registrationId; @@ -1933,7 +1958,7 @@ public async ValueTask DisposeAsync() return; } - _eventChannel.Writer.TryComplete(); + CloseEventChannel(); try { diff --git a/dotnet/test/E2E/SessionEventLoopLeakE2ETests.cs b/dotnet/test/E2E/SessionEventLoopLeakE2ETests.cs new file mode 100644 index 0000000000..40470109c5 --- /dev/null +++ b/dotnet/test/E2E/SessionEventLoopLeakE2ETests.cs @@ -0,0 +1,198 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.Test.Harness; +using System.Collections; +using System.Reflection; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.Test.E2E; + +/// +/// Regression coverage for the goroutine/task leak fixed alongside +/// github/copilot-sdk#2360: and +/// construct a +/// and start its event-dispatch consumer (ProcessEventsAsync) eagerly, before the +/// CLI confirms the session, so the CLI can route session-scoped requests to it while +/// session.create (or session.resume) is still being processed. Every failure path must +/// stop that consumer — otherwise each failed call leaks a background task forever, since +/// no caller ever receives the failed session to dispose it. Mirrors +/// go/internal/e2e/session_event_loop_leak_e2e_test.go. +/// +[Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] +public class SessionEventLoopLeakE2ETests(E2ETestFixture fixture, ITestOutputHelper output) + : E2ETestBase(fixture, "session-event-loop-leak", output) +{ + private static readonly FieldInfo SessionsField = + typeof(CopilotClient).GetField("_sessions", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("CopilotClient._sessions was not found."); + + private static readonly FieldInfo EventChannelField = + typeof(CopilotSession).GetField("_eventChannel", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("CopilotSession._eventChannel was not found."); + + private static IDictionary GetSessionsMap(CopilotClient client) => + (IDictionary)SessionsField.GetValue(client)!; + + private static bool IsEventChannelClosed(CopilotSession session) + { + var channel = EventChannelField.GetValue(session)!; + var readerProperty = channel.GetType().GetProperty("Reader") + ?? throw new InvalidOperationException("Channel.Reader was not found."); + var reader = readerProperty.GetValue(channel)!; + var completionProperty = reader.GetType().GetProperty("Completion") + ?? throw new InvalidOperationException("ChannelReader.Completion was not found."); + var completion = (Task)completionProperty.GetValue(reader)!; + return completion.IsCompleted; + } + + /// + /// Continuously scans 's session dictionary on a dedicated, + /// tightly-spinning thread (not the thread pool, and no await-based yielding) so + /// that even a sub-millisecond in-flight registration window — as seen with a fast local + /// RPC failure like a nonexistent-session resume — is reliably observed. + /// + private sealed class SessionSniffer : IDisposable + { + private readonly List _seen = []; + private readonly Thread _thread; + private volatile bool _stop; + + public SessionSniffer(IDictionary sessions) + { + _thread = new Thread(() => + { + var iterations = 0L; + while (!_stop) + { + iterations++; + foreach (CopilotSession s in sessions.Values) + { + lock (_seen) + { + if (!_seen.Contains(s)) _seen.Add(s); + } + } + } + Iterations = iterations; + }) + { IsBackground = true }; + _thread.Start(); + } + + public long Iterations { get; private set; } + + public IReadOnlyList Stop() + { + _stop = true; + _thread.Join(); + lock (_seen) return [.. _seen]; + } + + public void Dispose() => _stop = true; + } + + [Fact] + public async Task CreateSessionAsync_Failure_Does_Not_Leak_The_Session_Or_Its_Event_Loop() + { + // An invalid per-session GitHub token, redirected at the replaying proxy, makes + // the real CLI reject session.create with a genuine RPC error (401 Unauthorized) + // — the same failure path a real user would hit, not a mocked transport. + var env = new Dictionary(Ctx.GetEnvironment()) + { + ["COPILOT_DEBUG_GITHUB_API_URL"] = Ctx.ProxyUrl, + }; + var client = Ctx.CreateClient(environment: env, autoInjectGitHubToken: false); + + async Task CreateFailingAsync() + { + var ex = await Assert.ThrowsAnyAsync(() => Ctx.CreateSessionAsync(client, new SessionConfig + { + GitHubToken = "invalid-token", + OnPermissionRequest = PermissionHandler.ApproveAll, + })); + Assert.Contains("401", ex.ToString(), StringComparison.OrdinalIgnoreCase); + } + + // Warm up: the first call establishes the CLI connection. + await CreateFailingAsync(); + + var sessions = GetSessionsMap(client); + + // The session is registered (and its event-loop consumer started) before the RPC + // completes, and is only ever removed inside CreateSessionAsync's own catch block — + // by the time a failed call *returns* to us, RemoveFromClient() has already run, so + // polling the dictionary after each await observes nothing. We must instead observe + // it concurrently, while each call is still in flight, to capture the real session + // object and verify its event channel actually got closed. + using var sniffer = new SessionSniffer(sessions); + + for (var i = 0; i < 20; i++) + { + await CreateFailingAsync(); + } + + var seen = sniffer.Stop(); + + Assert.True(seen.Count > 0, "Test did not observe any in-flight session registrations; cannot validate the fix."); + Assert.Empty(sessions); + + foreach (var s in seen) + { + Assert.True(IsEventChannelClosed(s), "A failed CreateSessionAsync's session had its event channel left open, leaking its background event-processing task."); + } + } + + [Fact] + public async Task ResumeSessionAsync_Failure_Does_Not_Leak_The_Session_Or_Its_Event_Loop() + { + // Use our own dedicated client rather than the shared fixture's ResumeSessionAsync + // helper: that helper spins up a brand-new CopilotClient for every call it makes + // (to support the multi-client resume scenarios it's designed for), so each call's + // pre-registered session would land in a different, short-lived client's dictionary + // that we'd never get to observe. A single, reused client lets us watch one + // dictionary across all 20 failed calls. + var client = Ctx.CreateClient(); + var sessions = GetSessionsMap(client); + + async Task ResumeNonExistentAsync() + { + await Assert.ThrowsAnyAsync(() => + Ctx.ResumeSessionAsync(client, "non-existent-leak-check-session", new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + })); + } + + // Warm up: the first call establishes the CLI connection. + await ResumeNonExistentAsync(); + + // Same rationale as the CreateSessionAsync test above: the pre-registered session is + // already removed from the dictionary by the time a failed call returns, so we must + // observe it concurrently, while the call is still in flight, to actually validate + // that its event channel got closed rather than just that it got unregistered. + using var sniffer = new SessionSniffer(sessions); + + var baseline = sessions.Count; + for (var i = 0; i < 20; i++) + { + await ResumeNonExistentAsync(); + } + + var seen = sniffer.Stop(); + + Assert.True(seen.Count > 0, $"Test did not observe any in-flight session registrations (sniffer ran {sniffer.Iterations} iterations); cannot validate the fix."); + + Assert.True( + sessions.Count == baseline, + $"Expected no sessions left registered after 20 failed ResumeSessionAsync calls (baseline={baseline}), " + + $"but found {sessions.Count}. Failed ResumeSessionAsync calls must not leak the local session registration."); + + foreach (var s in seen) + { + Assert.True(IsEventChannelClosed(s), "A failed ResumeSessionAsync's session had its event channel left open, leaking its background event-processing task."); + } + } +} diff --git a/go/client.go b/go/client.go index 5789fca5eb..2f5888c93a 100644 --- a/go/client.go +++ b/go/client.go @@ -985,6 +985,19 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses } req.SessionID = localSessionID + // unregisterSession removes only the session created by this call and stops + // its event consumer. The latter is essential on CreateSession error paths: + // newSession starts processEvents eagerly, and no caller receives the failed + // session to disconnect it. + unregisterSession := func(sessionID string, s *Session) { + c.sessionsMux.Lock() + if c.sessions[sessionID] == s { + delete(c.sessions, sessionID) + } + c.sessionsMux.Unlock() + s.stopEventProcessing() + } + // initializeSession creates the session, wires up handlers, and registers // it in the sessions map. Invoked from the read loop the instant the // session.create response arrives (synchronously, before the next @@ -1038,17 +1051,13 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if c.options.SessionFS != nil { if config.CreateSessionFSProvider == nil { - c.sessionsMux.Lock() - delete(c.sessions, sessionID) - c.sessionsMux.Unlock() + unregisterSession(sessionID, s) return nil, fmt.Errorf("CreateSessionFSProvider is required in session config when SessionFS is enabled in client options") } provider := config.CreateSessionFSProvider(s) if c.options.SessionFS.Capabilities != nil && c.options.SessionFS.Capabilities.Sqlite { if _, ok := provider.(SessionFSSqliteProvider); !ok { - c.sessionsMux.Lock() - delete(c.sessions, sessionID) - c.sessionsMux.Unlock() + unregisterSession(sessionID, s) return nil, fmt.Errorf("SessionFS capabilities declare SQLite support but the provider does not implement SessionFSSqliteProvider") } } @@ -1105,9 +1114,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses result, err := c.client.RequestWithInlineResponse(ctx, "session.create", req, inlineCb) if err != nil { if registeredSessionID != "" { - c.sessionsMux.Lock() - delete(c.sessions, registeredSessionID) - c.sessionsMux.Unlock() + unregisterSession(registeredSessionID, session) } return nil, fmt.Errorf("failed to create session: %w", err) } @@ -1115,9 +1122,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses var response createSessionResponse if err := json.Unmarshal(result, &response); err != nil { if registeredSessionID != "" { - c.sessionsMux.Lock() - delete(c.sessions, registeredSessionID) - c.sessionsMux.Unlock() + unregisterSession(registeredSessionID, session) } return nil, fmt.Errorf("failed to unmarshal response: %w", err) } @@ -1127,9 +1132,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses } if localSessionID != "" && response.SessionID != "" && response.SessionID != localSessionID { - c.sessionsMux.Lock() - delete(c.sessions, registeredSessionID) - c.sessionsMux.Unlock() + unregisterSession(registeredSessionID, session) return nil, fmt.Errorf("session.create returned sessionId %s but the caller requested %s", response.SessionID, localSessionID) } if config.OnMCPAuthRequest != nil { @@ -1411,6 +1414,11 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } } c.sessionsMux.Unlock() + // newSession starts processEvents eagerly, before the RPC confirms the + // resume; every failure path here restores the previously-registered + // session (if any) but never returns this failed one to the caller, so + // its event consumer must be stopped here or it leaks forever. + session.stopEventProcessing() } if c.options.SessionFS != nil { diff --git a/go/client_test.go b/go/client_test.go index dffc15d74e..db2c3b0f28 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -924,6 +924,225 @@ func sessionIDFromParams(t *testing.T, params json.RawMessage) string { return decoded.SessionID } +func TestClient_CreateSessionFailureClosesRegisteredSession(t *testing.T) { + tests := []struct { + name string + response func(string) (json.RawMessage, *jsonrpc2.Error) + wantErrSub string + }{ + { + name: "RPC failure", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "session creation failed"} + }, + wantErrSub: "failed to create session", + }, + { + name: "invalid response", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return json.RawMessage(`"invalid"`), nil + }, + wantErrSub: "failed to unmarshal response", + }, + { + name: "session ID mismatch", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return json.RawMessage(`{"sessionId":"different-session"}`), nil + }, + wantErrSub: "but the caller requested failed-session", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + captured := make(chan *Session, 1) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + client.sessionsMux.Lock() + session := client.sessions[sessionID] + client.sessionsMux.Unlock() + captured <- session + return tt.response(sessionID) + }) + + _, err := client.CreateSession(t.Context(), &SessionConfig{SessionID: "failed-session"}) + if err == nil || !strings.Contains(err.Error(), tt.wantErrSub) { + t.Fatalf("CreateSession error = %v, want substring %q", err, tt.wantErrSub) + } + + session := <-captured + if session == nil { + t.Fatal("session was not registered before session.create") + } + assertSessionEventChannelClosed(t, session) + assertSessionNotRegistered(t, client, "failed-session") + }) + } +} + +func TestClient_CreateSessionInitializationFailureClosesRegisteredSession(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + options: ClientOptions{SessionFS: &SessionFSConfig{ + InitialWorkingDirectory: "/", + SessionStatePath: "/session-state", + Conventions: rpc.SessionFSSetProviderConventionsPosix, + Capabilities: &SessionFSCapabilities{Sqlite: true}, + }}, + } + + var captured *Session + _, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: "failed-session-fs", + CreateSessionFSProvider: func(session *Session) SessionFSProvider { + captured = session + return noSQLiteSessionFSProvider{} + }, + }) + if err == nil || !strings.Contains(err.Error(), "does not implement SessionFSSqliteProvider") { + t.Fatalf("CreateSession error = %v, want SQLite provider validation error", err) + } + if captured == nil { + t.Fatal("CreateSessionFSProvider did not receive the registered session") + } + assertSessionEventChannelClosed(t, captured) + assertSessionNotRegistered(t, client, "failed-session-fs") +} + +func TestClient_ResumeSessionFailureClosesRegisteredSession(t *testing.T) { + tests := []struct { + name string + response func(string) (json.RawMessage, *jsonrpc2.Error) + wantErrSub string + }{ + { + name: "RPC failure", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "session resume failed"} + }, + wantErrSub: "failed to resume session", + }, + { + name: "invalid response", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return json.RawMessage(`"invalid"`), nil + }, + wantErrSub: "failed to unmarshal response", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + captured := make(chan *Session, 1) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + client.sessionsMux.Lock() + session := client.sessions[sessionID] + client.sessionsMux.Unlock() + captured <- session + return tt.response(sessionID) + }) + + _, err := client.ResumeSession(t.Context(), "resumed-session", &ResumeSessionConfig{}) + if err == nil || !strings.Contains(err.Error(), tt.wantErrSub) { + t.Fatalf("ResumeSession error = %v, want substring %q", err, tt.wantErrSub) + } + + session := <-captured + if session == nil { + t.Fatal("session was not registered before session.resume") + } + assertSessionEventChannelClosed(t, session) + assertSessionNotRegistered(t, client, "resumed-session") + }) + } +} + +func TestClient_ResumeSessionInitializationFailureClosesRegisteredSession(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + options: ClientOptions{SessionFS: &SessionFSConfig{ + InitialWorkingDirectory: "/", + SessionStatePath: "/session-state", + Conventions: rpc.SessionFSSetProviderConventionsPosix, + Capabilities: &SessionFSCapabilities{Sqlite: true}, + }}, + } + + var captured *Session + _, err := client.ResumeSession(t.Context(), "resumed-session-fs", &ResumeSessionConfig{ + CreateSessionFSProvider: func(session *Session) SessionFSProvider { + captured = session + return noSQLiteSessionFSProvider{} + }, + }) + if err == nil || !strings.Contains(err.Error(), "does not implement SessionFSSqliteProvider") { + t.Fatalf("ResumeSession error = %v, want SQLite provider validation error", err) + } + if captured == nil { + t.Fatal("CreateSessionFSProvider did not receive the registered session") + } + assertSessionEventChannelClosed(t, captured) + assertSessionNotRegistered(t, client, "resumed-session-fs") +} + +func assertSessionEventChannelClosed(t *testing.T, session *Session) { + t.Helper() + select { + case <-session.eventDone: + case <-time.After(time.Second): + t.Fatal("timed out waiting for session event processing to stop") + } +} + +func assertSessionNotRegistered(t *testing.T, client *Client, sessionID string) { + t.Helper() + client.sessionsMux.Lock() + defer client.sessionsMux.Unlock() + if _, ok := client.sessions[sessionID]; ok { + t.Fatalf("session %q is still registered", sessionID) + } +} + +type noSQLiteSessionFSProvider struct{} + +func (noSQLiteSessionFSProvider) ReadFile(string) (string, error) { return "", nil } +func (noSQLiteSessionFSProvider) WriteFile(string, string, *int) error { return nil } +func (noSQLiteSessionFSProvider) AppendFile(string, string, *int) error { return nil } +func (noSQLiteSessionFSProvider) Exists(string) (bool, error) { return false, nil } +func (noSQLiteSessionFSProvider) Stat(string) (*SessionFSFileInfo, error) { return nil, nil } +func (noSQLiteSessionFSProvider) MakeDirectory(string, bool, *int) error { return nil } +func (noSQLiteSessionFSProvider) ReadDirectory(string) ([]string, error) { return nil, nil } +func (noSQLiteSessionFSProvider) ReadDirectoryWithTypes(string) ([]rpc.SessionFSReaddirWithTypesEntry, error) { + return nil, nil +} +func (noSQLiteSessionFSProvider) Remove(string, bool, bool) error { return nil } +func (noSQLiteSessionFSProvider) Rename(string, string) error { return nil } + func assertRuntimeShutdownNotCalled(t *testing.T, shutdownCalled <-chan struct{}) { t.Helper() select { diff --git a/go/internal/e2e/session_event_loop_leak_e2e_test.go b/go/internal/e2e/session_event_loop_leak_e2e_test.go new file mode 100644 index 0000000000..c6974f74d8 --- /dev/null +++ b/go/internal/e2e/session_event_loop_leak_e2e_test.go @@ -0,0 +1,102 @@ +package e2e + +import ( + "runtime" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" +) + +// TestSessionEventLoopLeakE2E is a regression test for the goroutine leak fixed +// alongside PR #2360: CreateSession/ResumeSession construct a *Session and start +// its event-dispatch goroutine eagerly, before the server confirms the session, +// so the CLI can route session-scoped requests to it while session.create (or +// session.resume) is still being processed. Every failure path must stop that +// goroutine — otherwise each failed call leaks one goroutine forever, since no +// caller ever receives the failed session to Disconnect() it. This test fails +// without the fix and passes with it. +func TestSessionEventLoopLeakE2E(t *testing.T) { + ctx := testharness.NewTestContext(t) + + t.Run("CreateSession failure does not leak the event loop goroutine", func(t *testing.T) { + ctx.ConfigureForTest(t) + + // Redirect the CLI's GitHub API calls at the replaying proxy so an + // invalid per-session token makes the real CLI reject session.create + // with a genuine RPC error (401 Unauthorized), exercising the same + // failure path a real user would hit — not a mocked transport. + client := ctx.NewClient(func(opts *copilot.ClientOptions) { + opts.Env = append(opts.Env, "COPILOT_DEBUG_GITHUB_API_URL="+ctx.ProxyURL) + }) + t.Cleanup(func() { client.ForceStop() }) + + createFailing := func() { + if _, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + GitHubToken: "invalid-token", + }); err == nil { + t.Fatal("expected CreateSession to fail with an invalid token") + } + } + + // Warm up: the first call spawns the CLI subprocess and its steady-state + // goroutines (read loop, etc.), which must not be counted as leaks. + createFailing() + + assertNoGoroutineLeak(t, 20, createFailing) + }) + + t.Run("ResumeSession failure does not leak the event loop goroutine", func(t *testing.T) { + ctx.ConfigureForTest(t) + + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + resumeNonExistent := func() { + if _, err := client.ResumeSession(t.Context(), "non-existent-leak-check-session", &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }); err == nil { + t.Fatal("expected ResumeSession for a non-existent session to fail") + } + } + + // Warm up: the first call spawns the CLI subprocess and its steady-state + // goroutines (read loop, etc.), which must not be counted as leaks. + resumeNonExistent() + + assertNoGoroutineLeak(t, 20, resumeNonExistent) + }) +} + +// assertNoGoroutineLeak runs fn n times and fails if the goroutine count grows +// roughly in proportion to n afterward, which would indicate one goroutine +// leaked per call rather than transient goroutines that already exited. +func assertNoGoroutineLeak(t *testing.T, n int, fn func()) { + t.Helper() + runtime.GC() + before := runtime.NumGoroutine() + + for i := 0; i < n; i++ { + fn() + } + + // Give any goroutines that exit promptly (but not synchronously with the + // call returning) a brief window to actually terminate before measuring. + deadline := time.Now().Add(2 * time.Second) + var after int + for { + runtime.GC() + after = runtime.NumGoroutine() + if after <= before+3 || time.Now().After(deadline) { + break + } + time.Sleep(50 * time.Millisecond) + } + + t.Logf("goroutines before=%d after=%d (n=%d)", before, after, n) + if after > before+3 { + t.Fatalf("goroutine count grew from %d to %d after %d failed calls; suspected event-loop leak", before, after, n) + } +} diff --git a/go/session.go b/go/session.go index 32fed684d9..4c31c01e8c 100644 --- a/go/session.go +++ b/go/session.go @@ -1433,6 +1433,9 @@ func (s *Session) processEvents() { } } +// stopEventProcessing stops the session event consumer without making an RPC. +// CreateSession/ResumeSession use this when a locally registered session fails +// before it can be returned to the caller. func (s *Session) stopEventProcessing() { s.closeOnce.Do(func() { close(s.eventDone) }) } From 08543eeba3b175728f3569f7c915ccd9b53cf310 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:39:17 +0000 Subject: [PATCH 42/60] build(deps-dev): bump fast-uri from 3.1.5 to 3.1.7 in /test/harness (#2488) Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.5 to 3.1.7. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Steve Sanderson Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/harness/package-lock.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 2488aa2c01..547e0a6ec9 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -1378,7 +1378,8 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.5", + "version": "3.1.7", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "dev": true, "funding": [ { From 67f4f12bf9afd26f06209263ffdb7be4658d6761 Mon Sep 17 00:00:00 2001 From: Mark Xian Date: Thu, 3 Sep 2026 17:20:20 +0000 Subject: [PATCH 43/60] fix: support bracketed IPv6 runtime URLs (#2200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: support bracketed IPv6 runtime URLs * fix: support bracketed IPv6 runtime URLs across SDKs * fix: validate bracketed IPv6 runtime hosts * fix: order Python imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: 冼健聪 Co-authored-by: Steve Sanderson Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 4 +- .../Unit/RuntimeConnectionUrlParsingTests.cs | 42 +++++++++++++++++++ go/client.go | 25 +++++++++-- go/client_test.go | 27 ++++++++++++ .../com/github/copilot/CopilotClient.java | 5 ++- .../github/copilot/CliServerManagerTest.java | 7 ++++ .../com/github/copilot/CopilotClientTest.java | 9 ++++ nodejs/src/client.ts | 24 +++++++++-- nodejs/test/client.test.ts | 31 ++++++++++++++ python/copilot/client.py | 40 +++++++++++------- python/test_client.py | 28 +++++++++++++ 11 files changed, 215 insertions(+), 27 deletions(-) create mode 100644 dotnet/test/Unit/RuntimeConnectionUrlParsingTests.cs diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 2298edc0ce..b02958ea97 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -177,7 +177,7 @@ public CopilotClient(CopilotClientOptions? options = null) throw new ArgumentException("GitHubToken and UseLoggedInUser cannot be combined with RuntimeConnection.ForUri (the existing runtime manages its own auth).", nameof(options)); } var parsed = ParseRuntimeUrl(uri.Url); - _optionsHost = parsed.Host; + _optionsHost = parsed.Host.Trim('[', ']'); _optionsPort = parsed.Port; break; @@ -308,7 +308,7 @@ private static RuntimeConnection ResolveDefaultConnection(CopilotClientOptions o /// /// Parses a runtime URL into a URI with host and port. /// - /// The URL to parse. Supports formats: "port", "host:port", "http://host:port". + /// The URL to parse. Supports formats: "port", "host:port", "[ipv6]:port", "http://host:port". private static Uri ParseRuntimeUrl(string url) { // If it's just a port number, treat as localhost diff --git a/dotnet/test/Unit/RuntimeConnectionUrlParsingTests.cs b/dotnet/test/Unit/RuntimeConnectionUrlParsingTests.cs new file mode 100644 index 0000000000..1b16456b0d --- /dev/null +++ b/dotnet/test/Unit/RuntimeConnectionUrlParsingTests.cs @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using Xunit; +using System.Reflection; + +namespace GitHub.Copilot.Test.Unit; + +public class RuntimeConnectionUrlParsingTests +{ + [Fact] + public void ForUri_ParsesBracketedIpv6HostPort() + { + var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri("[::1]:9000") + }); + + Assert.Equal("::1", GetPrivateField(client, "_optionsHost")); + Assert.Equal(9000, GetPrivateField(client, "_optionsPort")); + } + + [Fact] + public void ForUri_ParsesHttpIpv6HostPort() + { + var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri("http://[::1]:7000") + }); + + Assert.Equal("::1", GetPrivateField(client, "_optionsHost")); + Assert.Equal(7000, GetPrivateField(client, "_optionsPort")); + } + + private static T? GetPrivateField(object instance, string name) + { + var field = instance.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(field); + return (T?)field.GetValue(instance); + } +} diff --git a/go/client.go b/go/client.go index 2f5888c93a..ba77402ddb 100644 --- a/go/client.go +++ b/go/client.go @@ -36,6 +36,7 @@ import ( "fmt" "log" "net" + "net/netip" "os" "os/exec" "path/filepath" @@ -401,13 +402,31 @@ func setEnvValue(env []string, key string, value string) []string { // parseCLIURL parses a CLI URL into host and port components. // -// Supports formats: "host:port", "http://host:port", "https://host:port", or just "port". +// Supports formats: "host:port", "[ipv6]:port", "http://host:port", "https://host:port", or just "port". // Panics if the URL format is invalid or the port is out of range. func parseCLIURL(url string) (string, int) { // Remove protocol if present cleanURL, _ := strings.CutPrefix(url, "https://") cleanURL, _ = strings.CutPrefix(cleanURL, "http://") + // Use the standard parser only for the bracketed IPv6 form. Keep the + // existing host:port parsing behavior for all other inputs. + if strings.HasPrefix(cleanURL, "[") { + host, portStr, err := net.SplitHostPort(cleanURL) + if err != nil { + panic(fmt.Sprintf("Invalid port in URIConnection: %s", url)) + } + addr, err := netip.ParseAddr(host) + if err != nil || !addr.Is6() { + panic(fmt.Sprintf("Invalid URIConnection format: %s", url)) + } + port, err := strconv.Atoi(portStr) + if err != nil || port <= 0 || port > 65535 { + panic(fmt.Sprintf("Invalid port in URIConnection: %s", url)) + } + return host, port + } + // Parse host:port or port format var host string var portStr string @@ -415,15 +434,13 @@ func parseCLIURL(url string) (string, int) { host = before portStr = after } else { - // Only port provided - portStr = before + portStr = cleanURL } if host == "" { host = "localhost" } - // Validate port port, err := strconv.Atoi(portStr) if err != nil || port <= 0 || port > 65535 { panic(fmt.Sprintf("Invalid port in URIConnection: %s", url)) diff --git a/go/client_test.go b/go/client_test.go index db2c3b0f28..ee0e078fb1 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -78,6 +78,15 @@ func TestClient_URLParsing(t *testing.T) { } }) + t.Run("should parse bracketed IPv6 host:port URL format", func(t *testing.T) { + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: "[::1]:9000"}, + }) + if client.actualPort != 9000 || client.actualHost != "::1" { + t.Errorf("Expected [::1]:9000, got %s:%d", client.actualHost, client.actualPort) + } + }) + t.Run("should parse http://host:port URL format", func(t *testing.T) { client := NewClient(&ClientOptions{ Connection: URIConnection{URL: "http://localhost:7000"}, @@ -87,6 +96,24 @@ func TestClient_URLParsing(t *testing.T) { } }) + t.Run("should parse http://[ipv6]:port URL format", func(t *testing.T) { + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: "http://[::1]:7000"}, + }) + if client.actualPort != 7000 || client.actualHost != "::1" { + t.Errorf("Expected [::1]:7000, got %s:%d", client.actualHost, client.actualPort) + } + }) + + t.Run("should panic for bracketed non-IPv6 host", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic for invalid bracketed host") + } + }() + NewClient(&ClientOptions{Connection: URIConnection{URL: "[not-ipv6]:1234"}}) + }) + t.Run("should parse https://host:port URL format", func(t *testing.T) { client := NewClient(&ClientOptions{ Connection: URIConnection{URL: "https://example.com:443"}, diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index c986e829a4..56c3487635 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -221,7 +221,10 @@ public CopilotClient(CopilotClientOptions options) { // Parse CliUrl if provided if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) { URI uri = CliServerManager.parseCliUrl(this.options.getCliUrl()); - this.optionsHost = uri.getHost(); + String host = uri.getHost(); + this.optionsHost = host != null && host.startsWith("[") && host.endsWith("]") + ? host.substring(1, host.length() - 1) + : host; this.optionsPort = uri.getPort(); } else { this.optionsHost = null; diff --git a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java index a227be04b9..b41f99adb7 100644 --- a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java @@ -93,6 +93,13 @@ void parseCliUrlWithHttpsPrefix() { assertEquals("https://secure.host:443", uri.toString()); } + @Test + void parseCliUrlWithBracketedIpv6() { + URI uri = CliServerManager.parseCliUrl("[::1]:4321"); + assertNotNull(uri.getHost()); + assertEquals(4321, uri.getPort()); + } + @Test void parseCliUrlWithHostOnly() { URI uri = CliServerManager.parseCliUrl("copilot.example.com"); diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java index 4cfd7f4c48..db40d425f4 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java @@ -179,6 +179,15 @@ void testCliUrlOnlyConstruction() { client.close(); } + @Test + void testBracketedIpv6CliUrlNormalizesHost() throws Exception { + try (var client = new CopilotClient(new CopilotClientOptions().setCliUrl("[::1]:4321"))) { + Field hostField = CopilotClient.class.getDeclaredField("optionsHost"); + hostField.setAccessible(true); + assertEquals("::1", hostField.get(client)); + } + } + @Test void testCliUrlMutualExclusionWithCliPath() { var options = new CopilotClientOptions().setCliUrl("localhost:3000").setCliPath("/path/to/cli"); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 51c3e6650d..a942ed6776 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -14,7 +14,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; -import { Socket } from "node:net"; +import { isIPv6, Socket } from "node:net"; import { dirname, isAbsolute, join, resolve } from "node:path"; import { createMessageConnection, @@ -748,22 +748,38 @@ export class CopilotClient { /** * Parse CLI URL into host and port - * Supports formats: "host:port", "http://host:port", "https://host:port", or just "port" + * Supports formats: "host:port", "[ipv6]:port", "http://host:port", "https://host:port", or just "port" */ private parseCliUrl(url: string): { host: string; port: number } { // Remove protocol if present - let cleanUrl = url.replace(/^https?:\/\//, ""); + const cleanUrl = url.replace(/^https?:\/\//, ""); // Check if it's just a port number if (/^\d+$/.test(cleanUrl)) { return { host: "localhost", port: parseInt(cleanUrl, 10) }; } + // Handle the canonical bracketed IPv6 host:port form without changing + // the existing parser behavior for other inputs. + const ipv6Match = cleanUrl.match(/^\[([^\]]+)\]:(\d+)$/); + if (ipv6Match) { + const host = ipv6Match[1]; + if (!isIPv6(host)) { + throw new Error(`Invalid cliUrl format: ${url}`); + } + + const port = parseInt(ipv6Match[2], 10); + if (isNaN(port) || port <= 0 || port > 65535) { + throw new Error(`Invalid port in cliUrl: ${url}`); + } + return { host, port }; + } + // Parse host:port format const parts = cleanUrl.split(":"); if (parts.length !== 2) { throw new Error( - `Invalid cliUrl format: ${url}. Expected "host:port", "http://host:port", or "port"` + `Invalid cliUrl format: ${url}. Expected "host:port", "[ipv6]:port", "http://host:port", or "port"` ); } diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index bd1cf9812f..2abba2fcb6 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -2627,6 +2627,17 @@ describe("CopilotClient", () => { expect((client as any).isExternalServer).toBe(true); }); + it("should parse bracketed IPv6 host:port URL format", () => { + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("[::1]:9000"), + logLevel: "error", + }); + + expect((client as any).runtimePort).toBe(9000); + expect((client as any).actualHost).toBe("::1"); + expect((client as any).isExternalServer).toBe(true); + }); + it("should parse http://host:port URL format", () => { const client = new CopilotClient({ connection: RuntimeConnection.forUri("http://localhost:7000"), @@ -2638,6 +2649,26 @@ describe("CopilotClient", () => { expect((client as any).isExternalServer).toBe(true); }); + it("should parse http://[ipv6]:port URL format", () => { + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("http://[::1]:7000"), + logLevel: "error", + }); + + expect((client as any).runtimePort).toBe(7000); + expect((client as any).actualHost).toBe("::1"); + expect((client as any).isExternalServer).toBe(true); + }); + + it("should reject a bracketed non-IPv6 host", () => { + expect(() => { + new CopilotClient({ + connection: RuntimeConnection.forUri("[not-ipv6]:1234"), + logLevel: "error", + }); + }).toThrow(/Invalid cliUrl format/); + }); + it("should parse https://host:port URL format", () => { const client = new CopilotClient({ connection: RuntimeConnection.forUri("https://example.com:443"), diff --git a/python/copilot/client.py b/python/copilot/client.py index f4d7da75a7..4a391e6a29 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -16,6 +16,7 @@ import asyncio import inspect +import ipaddress import logging import os import re @@ -1838,8 +1839,8 @@ def _parse_cli_url(self, url: str) -> tuple[str, int]: """ Parse CLI URL into host and port. - Supports formats: "host:port", "http://host:port", "https://host:port", - or just "port". + Supports formats: "host:port", "[ipv6]:port", "http://host:port", + "https://host:port", or just "port". Args: url: The CLI URL to parse. @@ -1850,9 +1851,6 @@ def _parse_cli_url(self, url: str) -> tuple[str, int]: Raises: ValueError: If the URL format is invalid or the port is out of range. """ - import re - - # Remove protocol if present clean_url = re.sub(r"^https?://", "", url) # Check if it's just a port number @@ -1862,14 +1860,24 @@ def _parse_cli_url(self, url: str) -> tuple[str, int]: raise ValueError(f"Invalid port in cli_url: {url}") return ("localhost", port) - # Parse host:port format - parts = clean_url.split(":") - if len(parts) != 2: - raise ValueError(f"Invalid cli_url format: {url}") + ipv6_match = re.match(r"^\[([^\]]+)\]:(.*)$", clean_url) + if ipv6_match: + host = ipv6_match.group(1) + port_text = ipv6_match.group(2) + try: + ipaddress.IPv6Address(host) + except ValueError as e: + raise ValueError(f"Invalid cli_url format: {url}") from e + else: + # Parse host:port format + parts = clean_url.split(":") + if len(parts) != 2: + raise ValueError(f"Invalid cli_url format: {url}") + host = parts[0] if parts[0] else "localhost" + port_text = parts[1] - host = parts[0] if parts[0] else "localhost" try: - port = int(parts[1]) + port = int(port_text) except ValueError as e: raise ValueError(f"Invalid port in cli_url: {url}") from e @@ -4631,22 +4639,22 @@ async def _connect_via_tcp(self) -> None: if not self._runtime_port: raise RuntimeError("Server port not available") - # Create a TCP socket connection with timeout + # Create a TCP socket connection with timeout. create_connection resolves + # both IPv4 and IPv6 addresses instead of forcing AF_INET. import socket # Connection timeout constant TCP_CONNECTION_TIMEOUT = 10 # seconds - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(TCP_CONNECTION_TIMEOUT) - try: tcp_connect_start = time.perf_counter() logger.info( "CopilotClient._connect_via_tcp connecting to CLI server", extra={"host": self._actual_host, "port": self._runtime_port}, ) - sock.connect((self._actual_host, self._runtime_port)) + sock = socket.create_connection( + (self._actual_host, self._runtime_port), timeout=TCP_CONNECTION_TIMEOUT + ) sock.settimeout(None) # Remove timeout after connection log_timing( logger, diff --git a/python/test_client.py b/python/test_client.py index bf8de113ae..b9c74294f3 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -1510,12 +1510,28 @@ def test_parse_host_port_url(self): assert client._actual_host == "127.0.0.1" assert client._is_external_server + def test_parse_bracketed_ipv6_host_port_url(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("[::1]:9000")) + assert client._runtime_port == 9000 + assert client._actual_host == "::1" + assert client._is_external_server + def test_parse_http_url(self): client = CopilotClient(connection=RuntimeConnection.for_uri("http://localhost:7000")) assert client._runtime_port == 7000 assert client._actual_host == "localhost" assert client._is_external_server + def test_parse_http_ipv6_url(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("http://[::1]:7000")) + assert client._runtime_port == 7000 + assert client._actual_host == "::1" + assert client._is_external_server + + def test_reject_bracketed_non_ipv6_host(self): + with pytest.raises(ValueError, match="Invalid cli_url format"): + CopilotClient(connection=RuntimeConnection.for_uri("[not-ipv6]:1234")) + def test_parse_https_url(self): client = CopilotClient(connection=RuntimeConnection.for_uri("https://example.com:443")) assert client._runtime_port == 443 @@ -1542,6 +1558,18 @@ def test_is_external_server_true(self): client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:8080")) assert client._is_external_server + @pytest.mark.asyncio + async def test_connect_via_tcp_uses_family_independent_resolution(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("[::1]:9000")) + fake_socket = Mock() + fake_socket.makefile.return_value = Mock() + + with patch("socket.create_connection", return_value=fake_socket) as create_connection: + await client._connect_via_tcp() + + create_connection.assert_called_once_with(("::1", 9000), timeout=10) + client._process.terminate() + class TestSessionFsConfig: def test_missing_initial_cwd(self): From 1a56149dc106ee7d212f4db86cf39e378fa119b5 Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Thu, 3 Sep 2026 18:18:55 +0000 Subject: [PATCH 44/60] test: cover message identity compatibility across SDKs (#2468) * fix(rust): expose queued message identity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: adfd8891-a94a-4365-9d3a-9662f967d4cf * fix(rust): expose user message identity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: adfd8891-a94a-4365-9d3a-9662f967d4cf * test: cover message identity compatibility across SDKs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: adfd8891-a94a-4365-9d3a-9662f967d4cf --------- Copilot-Session: adfd8891-a94a-4365-9d3a-9662f967d4cf --- dotnet/test/Unit/SerializationTests.cs | 34 ++++++ .../Unit/SessionEventSerializationTests.cs | 34 ++++++ go/rpc/message_identity_test.go | 101 ++++++++++++++++++ .../MessageIdentitySerializationTest.java | 66 ++++++++++++ nodejs/test/message-identity-types.test.ts | 33 ++++++ python/test_message_identity_generated.py | 41 +++++++ rust/tests/api_types_test.rs | 40 ++++++- rust/tests/session_events_test.rs | 36 +++++++ 8 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 go/rpc/message_identity_test.go create mode 100644 java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java create mode 100644 nodejs/test/message-identity-types.test.ts create mode 100644 python/test_message_identity_generated.py create mode 100644 rust/tests/session_events_test.rs diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 2414093797..62dc7d001b 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -1122,6 +1122,40 @@ public void ToolResultObject_OmitsToolReferences_WhenNull_WithSdkOptions() Assert.False(document.RootElement.TryGetProperty("toolReferences", out _)); } +#pragma warning disable GHCP001 // The queue management surface is intentionally experimental. + [Theory] + [InlineData("message-1")] + [InlineData(null)] + public void QueuePendingItems_MessageId_UsesCamelCaseAndIsOptional(string? messageId) + { + var options = GetSerializerOptions(); + var messageIdProperty = messageId is null ? "" : $""","messageId":"{messageId}" """; + var json = $$""" + { + "id": "queue-1", + "kind": "message", + "displayText": "hello", + "agentMode": "interactive" + {{messageIdProperty}} + } + """; + + var item = JsonSerializer.Deserialize(json, options); + Assert.NotNull(item); + Assert.Equal(messageId, item.MessageId); + + using var document = JsonDocument.Parse(JsonSerializer.Serialize(item, options)); + if (messageId is null) + { + Assert.False(document.RootElement.TryGetProperty("messageId", out _)); + } + else + { + Assert.Equal(messageId, document.RootElement.GetProperty("messageId").GetString()); + } + } +#pragma warning restore GHCP001 + private static JsonSerializerOptions GetSerializerOptions() { var prop = typeof(CopilotClient) diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 8aea505737..25c56838bc 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -48,6 +48,40 @@ public void SessionEvent_Deserializes_AutoTier(AutoTier? expectedTier, string? w } } + [Theory] + [InlineData("message-1")] + [InlineData(null)] + public void UserMessageEvent_MessageId_UsesCamelCaseAndIsOptional(string? messageId) + { + var messageIdProperty = messageId is null ? "" : $""", "messageId": "{messageId}" """; + var json = $$""" + { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": "user.message", + "data": { + "content": "hello" + {{messageIdProperty}} + } + } + """; + + var sessionEvent = Assert.IsType(SessionEvent.FromJson(json)); + Assert.Equal(messageId, sessionEvent.Data.MessageId); + + using var document = JsonDocument.Parse(sessionEvent.ToJson()); + var data = document.RootElement.GetProperty("data"); + if (messageId is null) + { + Assert.False(data.TryGetProperty("messageId", out _)); + } + else + { + Assert.Equal(messageId, data.GetProperty("messageId").GetString()); + } + } + public static TheoryData JsonElementBackedEvents => new() { { diff --git a/go/rpc/message_identity_test.go b/go/rpc/message_identity_test.go new file mode 100644 index 0000000000..6124d7084a --- /dev/null +++ b/go/rpc/message_identity_test.go @@ -0,0 +1,101 @@ +package rpc + +import ( + "encoding/json" + "testing" +) + +func TestQueuePendingItemsMessageIDJSONCompatibility(t *testing.T) { + var item QueuePendingItems + if err := json.Unmarshal([]byte(`{ + "id": "queue-1", + "messageId": "message-1", + "kind": "message", + "displayText": "hello", + "agentMode": "interactive" + }`), &item); err != nil { + t.Fatal(err) + } + if item.MessageID == nil || *item.MessageID != "message-1" { + t.Fatalf("MessageID = %v, want message-1", item.MessageID) + } + + encoded, err := json.Marshal(item) + if err != nil { + t.Fatal(err) + } + var wire map[string]any + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatal(err) + } + if got := wire["messageId"]; got != "message-1" { + t.Fatalf("messageId = %v, want message-1", got) + } + + var olderItem QueuePendingItems + if err := json.Unmarshal([]byte(`{ + "id": "queue-2", + "kind": "command", + "displayText": "/help", + "agentMode": "interactive" + }`), &olderItem); err != nil { + t.Fatal(err) + } + if olderItem.MessageID != nil { + t.Fatalf("MessageID = %v, want nil", olderItem.MessageID) + } + + encoded, err = json.Marshal(olderItem) + if err != nil { + t.Fatal(err) + } + wire = nil + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatal(err) + } + if _, ok := wire["messageId"]; ok { + t.Fatal("messageId should be omitted when absent") + } +} + +func TestUserMessageDataMessageIDJSONCompatibility(t *testing.T) { + var message UserMessageData + if err := json.Unmarshal([]byte(`{"content":"hello","messageId":"message-1"}`), &message); err != nil { + t.Fatal(err) + } + if message.MessageID == nil || *message.MessageID != "message-1" { + t.Fatalf("MessageID = %v, want message-1", message.MessageID) + } + + encoded, err := json.Marshal(message) + if err != nil { + t.Fatal(err) + } + var wire map[string]any + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatal(err) + } + if got := wire["messageId"]; got != "message-1" { + t.Fatalf("messageId = %v, want message-1", got) + } + + var olderMessage UserMessageData + if err := json.Unmarshal([]byte(`{"content":"hello"}`), &olderMessage); err != nil { + t.Fatal(err) + } + if olderMessage.MessageID != nil { + t.Fatalf("MessageID = %v, want nil", olderMessage.MessageID) + } + + encoded, err = json.Marshal(olderMessage) + if err != nil { + t.Fatal(err) + } + wire = nil + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatal(err) + } + if _, ok := wire["messageId"]; ok { + t.Fatal("messageId should be omitted when absent") + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java b/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java new file mode 100644 index 0000000000..3ef4770562 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.UserMessageEvent.UserMessageEventData; +import com.github.copilot.generated.rpc.QueuePendingItems; + +class MessageIdentitySerializationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void testQueuePendingMessageIdUsesCamelCaseAndIsOptional() throws Exception { + var item = MAPPER.readValue(""" + { + "id": "queue-1", + "messageId": "message-1", + "kind": "message", + "displayText": "hello", + "agentMode": "interactive" + } + """, QueuePendingItems.class); + + assertEquals("message-1", item.messageId()); + assertEquals("message-1", MAPPER.valueToTree(item).get("messageId").textValue()); + + var olderItem = MAPPER.readValue(""" + { + "id": "queue-2", + "kind": "command", + "displayText": "/help", + "agentMode": "interactive" + } + """, QueuePendingItems.class); + + assertNull(olderItem.messageId()); + assertFalse(MAPPER.valueToTree(olderItem).has("messageId")); + } + + @Test + void testUserMessageIdUsesCamelCaseAndIsOptional() throws Exception { + var message = MAPPER.readValue(""" + {"content": "hello", "messageId": "message-1"} + """, UserMessageEventData.class); + + assertEquals("message-1", message.messageId()); + assertEquals("message-1", MAPPER.valueToTree(message).get("messageId").textValue()); + + var olderMessage = MAPPER.readValue(""" + {"content": "hello"} + """, UserMessageEventData.class); + + assertNull(olderMessage.messageId()); + assertFalse(MAPPER.valueToTree(olderMessage).has("messageId")); + } +} diff --git a/nodejs/test/message-identity-types.test.ts b/nodejs/test/message-identity-types.test.ts new file mode 100644 index 0000000000..903fc7533c --- /dev/null +++ b/nodejs/test/message-identity-types.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import type { QueuePendingItems } from "../src/generated/rpc.js"; +import type { UserMessageData } from "../src/generated/session-events.js"; + +describe("generated message identity types", () => { + it("exposes optional camelCase message IDs", () => { + const queueItemWithIdentity: QueuePendingItems = { + id: "queue-1", + messageId: "message-1", + kind: "message", + displayText: "hello", + agentMode: "interactive", + }; + const queueItemFromOlderRuntime: QueuePendingItems = { + id: "queue-2", + kind: "command", + displayText: "/help", + agentMode: "interactive", + }; + const userMessageWithIdentity: UserMessageData = { + content: "hello", + messageId: "message-1", + }; + const userMessageFromOlderRuntime: UserMessageData = { + content: "hello", + }; + + expect(queueItemWithIdentity.messageId).toBe("message-1"); + expect(queueItemFromOlderRuntime.messageId).toBeUndefined(); + expect(userMessageWithIdentity.messageId).toBe("message-1"); + expect(userMessageFromOlderRuntime.messageId).toBeUndefined(); + }); +}); diff --git a/python/test_message_identity_generated.py b/python/test_message_identity_generated.py new file mode 100644 index 0000000000..024223ac96 --- /dev/null +++ b/python/test_message_identity_generated.py @@ -0,0 +1,41 @@ +from copilot.generated.rpc import QueuePendingItems +from copilot.generated.session_events import UserMessageData + + +def test_queue_pending_message_id_uses_camel_case_and_is_optional(): + item = QueuePendingItems.from_dict( + { + "id": "queue-1", + "messageId": "message-1", + "kind": "message", + "displayText": "hello", + "agentMode": "interactive", + } + ) + + assert item.message_id == "message-1" + assert item.to_dict()["messageId"] == "message-1" + + older_item = QueuePendingItems.from_dict( + { + "id": "queue-2", + "kind": "command", + "displayText": "/help", + "agentMode": "interactive", + } + ) + + assert older_item.message_id is None + assert "messageId" not in older_item.to_dict() + + +def test_user_message_id_uses_camel_case_and_is_optional(): + message = UserMessageData.from_dict({"content": "hello", "messageId": "message-1"}) + + assert message.message_id == "message-1" + assert message.to_dict()["messageId"] == "message-1" + + older_message = UserMessageData.from_dict({"content": "hello"}) + + assert older_message.message_id is None + assert "messageId" not in older_message.to_dict() diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index 8ed40e7c76..942c5dab5b 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -6,7 +6,8 @@ use github_copilot_sdk::AutoTier; use github_copilot_sdk::rpc::{ Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, - ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest, + ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, QueuePendingItems, + QueuePendingItemsKind, SendAgentMode, TasksStartAgentRequest, }; use github_copilot_sdk::session_events::{ PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent, @@ -146,6 +147,43 @@ fn permission_event_exposes_managed_approval_required() { assert_eq!(request.managed_approval_required, Some(true)); } +#[test] +fn queue_pending_message_id_uses_camel_case_wire_name() { + let item = QueuePendingItems { + agent_mode: SendAgentMode::Interactive, + display_text: "second message".to_string(), + id: "batch-1".to_string(), + kind: QueuePendingItemsKind::Message, + message_id: Some("message-2".to_string()), + }; + + let serialized = serde_json::to_value(&item).unwrap(); + assert_eq!(serialized["id"], "batch-1"); + assert_eq!(serialized["messageId"], "message-2"); + + let deserialized: QueuePendingItems = serde_json::from_value(serialized).unwrap(); + assert_eq!(deserialized.message_id.as_deref(), Some("message-2")); +} + +#[test] +fn queue_pending_message_id_is_optional_for_older_hosts() { + let item: QueuePendingItems = serde_json::from_value(serde_json::json!({ + "agentMode": "interactive", + "displayText": "/model gpt-5", + "id": "command-1", + "kind": "command" + })) + .unwrap(); + + assert_eq!(item.message_id, None); + assert!( + serde_json::to_value(item) + .unwrap() + .get("messageId") + .is_none() + ); +} + fn running_extension(id: &str, name: &str) -> Extension { Extension { id: id.to_string(), diff --git a/rust/tests/session_events_test.rs b/rust/tests/session_events_test.rs new file mode 100644 index 0000000000..437be9861f --- /dev/null +++ b/rust/tests/session_events_test.rs @@ -0,0 +1,36 @@ +// Unit tests for generated session-event payloads. + +#![allow(clippy::unwrap_used)] + +use github_copilot_sdk::session_events::UserMessageData; + +#[test] +fn user_message_id_uses_camel_case_wire_name() { + let data = UserMessageData { + content: "queued message".to_string(), + message_id: Some("message-123".to_string()), + ..Default::default() + }; + + let serialized = serde_json::to_value(&data).unwrap(); + assert_eq!(serialized["messageId"], "message-123"); + + let deserialized: UserMessageData = serde_json::from_value(serialized).unwrap(); + assert_eq!(deserialized.message_id.as_deref(), Some("message-123")); +} + +#[test] +fn user_message_id_is_optional_for_older_hosts() { + let data: UserMessageData = serde_json::from_value(serde_json::json!({ + "content": "legacy message" + })) + .unwrap(); + + assert_eq!(data.message_id, None); + assert!( + serde_json::to_value(data) + .unwrap() + .get("messageId") + .is_none() + ); +} From e2af6c41180473269e02731d91c0dc11d414110d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 3 Sep 2026 17:35:44 -0400 Subject: [PATCH 45/60] Support non-object external tool arguments (#2496) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Session.cs | 60 ++++++- .../test/Unit/ClientSessionLifetimeTests.cs | 165 ++++++++++++++++++ 2 files changed, 223 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 24450a7c7d..d531238435 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -923,9 +923,16 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, if (arguments is JsonElement incomingJsonArgs) { - foreach (var prop in incomingJsonArgs.EnumerateObject()) + if (incomingJsonArgs.ValueKind == JsonValueKind.Object) { - aiFunctionArgs[prop.Name] = prop.Value; + foreach (var prop in incomingJsonArgs.EnumerateObject()) + { + aiFunctionArgs[prop.Name] = prop.Value; + } + } + else + { + aiFunctionArgs[GetSingleParameterName(tool)] = incomingJsonArgs; } } @@ -966,6 +973,55 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, // Connection already disposed — nothing we can do } } + + static string GetSingleParameterName(AIFunction tool) + { + if (tool.JsonSchema.TryGetProperty("properties", out var properties) && + properties.ValueKind == JsonValueKind.Object) + { + string? parameterName = null; + foreach (var property in properties.EnumerateObject()) + { + if (parameterName is not null) + { + parameterName = null; + break; + } + + parameterName = property.Name; + } + + if (parameterName is not null) + { + return parameterName; + } + + if (tool.JsonSchema.TryGetProperty("required", out var required) && + required.ValueKind == JsonValueKind.Array) + { + string? requiredParameterName = null; + foreach (var requiredParameter in required.EnumerateArray()) + { + if (requiredParameterName is not null) + { + requiredParameterName = null; + break; + } + + requiredParameterName = requiredParameter.GetString(); + } + + if (requiredParameterName is not null && + properties.TryGetProperty(requiredParameterName, out _)) + { + return requiredParameterName; + } + } + } + + throw new ArgumentException( + $"Tool '{tool.Name}' received non-object arguments, but its schema does not define exactly one parameter or one required parameter."); + } } /// diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index a59257b3da..204626a6f5 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -12,6 +12,7 @@ using System.Text; using System.Text.Json; using GitHub.Copilot.Rpc; +using Microsoft.Extensions.AI; using Xunit; namespace GitHub.Copilot.Test.Unit; @@ -665,6 +666,166 @@ public async Task SessionRequests_Serialize_Terminal_Tools() Assert.True(resumeRequest.Params.GetProperty("tools")[0].GetProperty("isTerminal").GetBoolean()); } + [Fact] + public async Task ExternalTool_String_Arguments_Bind_To_Single_Function_Parameter() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + string? receivedPatch = null; + var tool = CopilotTool.DefineTool( + (string patch) => + { + receivedPatch = patch; + return "applied"; + }, + new CopilotToolOptions { OverridesBuiltInTool = true }, + new AIFunctionFactoryOptions { Name = "apply_patch" }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + server.ClearRequests(); + using var arguments = JsonDocument.Parse("\"*** Begin Patch\\n*** End Patch\""); + + DispatchEvent(session, new ExternalToolRequestedEvent + { + Data = new ExternalToolRequestedData + { + Arguments = arguments.RootElement.Clone(), + RequestId = "apply-patch-request", + SessionId = session.SessionId, + ToolCallId = "apply-patch-call", + ToolName = "apply_patch" + } + }); + + var request = await WaitForRequestAsync(server, "session.tools.handlePendingToolCall"); + Assert.Equal("*** Begin Patch\n*** End Patch", receivedPatch); + Assert.False(request.Params.TryGetProperty("error", out _)); + Assert.Equal("applied", request.Params.GetProperty("result").GetProperty("textResultForLlm").GetString()); + } + + [Fact] + public async Task ExternalTool_String_Arguments_Reject_Ambiguous_Function_Parameters() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var invoked = false; + var tool = CopilotTool.DefineTool( + (string patch, string explanation) => + { + invoked = true; + return "applied"; + }, + new CopilotToolOptions { OverridesBuiltInTool = true }, + new AIFunctionFactoryOptions { Name = "apply_patch" }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + server.ClearRequests(); + using var arguments = JsonDocument.Parse("\"*** Begin Patch\\n*** End Patch\""); + + DispatchEvent(session, new ExternalToolRequestedEvent + { + Data = new ExternalToolRequestedData + { + Arguments = arguments.RootElement.Clone(), + RequestId = "ambiguous-apply-patch-request", + SessionId = session.SessionId, + ToolCallId = "ambiguous-apply-patch-call", + ToolName = "apply_patch" + } + }); + + var request = await WaitForRequestAsync(server, "session.tools.handlePendingToolCall"); + Assert.False(invoked); + Assert.Contains("received non-object arguments", request.Params.GetProperty("error").GetString()); + Assert.False(request.Params.TryGetProperty("result", out _)); + } + + [Fact] + public async Task ExternalTool_Number_Arguments_Bind_To_Single_Function_Parameter() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + int? receivedLine = null; + var tool = CopilotTool.DefineTool( + (int line) => + { + receivedLine = line; + return "selected"; + }, + factoryOptions: new AIFunctionFactoryOptions { Name = "select_line" }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + server.ClearRequests(); + using var arguments = JsonDocument.Parse("42"); + + DispatchEvent(session, new ExternalToolRequestedEvent + { + Data = new ExternalToolRequestedData + { + Arguments = arguments.RootElement.Clone(), + RequestId = "select-line-request", + SessionId = session.SessionId, + ToolCallId = "select-line-call", + ToolName = "select_line" + } + }); + + var request = await WaitForRequestAsync(server, "session.tools.handlePendingToolCall"); + Assert.Equal(42, receivedLine); + Assert.False(request.Params.TryGetProperty("error", out _)); + } + + [Fact] + public async Task ExternalTool_String_Arguments_Bind_To_Sole_Required_Function_Parameter() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + string? receivedPatch = null; + string? receivedExplanation = null; + var tool = CopilotTool.DefineTool( + (string patch, string? explanation = null) => + { + receivedPatch = patch; + receivedExplanation = explanation; + return "applied"; + }, + new CopilotToolOptions { OverridesBuiltInTool = true }, + new AIFunctionFactoryOptions { Name = "apply_patch" }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + server.ClearRequests(); + using var arguments = JsonDocument.Parse("\"*** Begin Patch\\n*** End Patch\""); + + DispatchEvent(session, new ExternalToolRequestedEvent + { + Data = new ExternalToolRequestedData + { + Arguments = arguments.RootElement.Clone(), + RequestId = "optional-apply-patch-request", + SessionId = session.SessionId, + ToolCallId = "optional-apply-patch-call", + ToolName = "apply_patch" + } + }); + + var request = await WaitForRequestAsync(server, "session.tools.handlePendingToolCall"); + Assert.Equal("*** Begin Patch\n*** End Patch", receivedPatch); + Assert.Null(receivedExplanation); + Assert.False(request.Params.TryGetProperty("error", out _)); + } + [Fact] public async Task EmptyMode_Create_Sends_Empty_IncludedBuiltinSkills() { @@ -1667,6 +1828,10 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel { ["success"] = true }, + "session.tools.handlePendingToolCall" => new Dictionary + { + ["success"] = true + }, "session.delete" => new Dictionary { ["success"] = true From 39c027b024e00d43f99bf0a9271b026023e9ba22 Mon Sep 17 00:00:00 2001 From: Devraj Mehta Date: Thu, 3 Sep 2026 21:37:57 +0000 Subject: [PATCH 46/60] Fix runtime release and workflow regressions (#2501) * Fix Node runtime package provenance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Fix release package verification Run Node package verification before artifact upload, validate source repository metadata at runtime, and honor explicit CLI paths for legacy .NET E2E connections. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Upgrade agentic workflows to gh-aw 0.88.2 Regenerate all workflow locks with the latest compiler, apply required explicit no-shell settings, and pin compiled-workflow verification to the same release. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Fix flaky shell and pending work E2E tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Fix flaky multi-client permission test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Normalize aborted shell cleanup results Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Keep replay fix surgical Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 * Assert idle event after permission denial Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 --------- Copilot-Session: 3043824d-becf-4b5d-b62b-4754511894f7 --- .gitattributes | 2 +- .github/aw/actions-lock.json | 11 +- .github/skills/agentic-workflows/SKILL.md | 18 + .github/workflows/agentics-maintenance.yml | 633 ------------- .../cross-repo-issue-analysis.lock.yml | 845 ++++++++++------- .github/workflows/handle-bug.lock.yml | 858 ++++++++++------- .github/workflows/handle-bug.md | 4 +- .../workflows/handle-documentation.lock.yml | 858 ++++++++++------- .github/workflows/handle-documentation.md | 4 +- .github/workflows/handle-enhancement.lock.yml | 858 ++++++++++------- .github/workflows/handle-enhancement.md | 4 +- .github/workflows/handle-question.lock.yml | 858 ++++++++++------- .github/workflows/handle-question.md | 4 +- .../workflows/issue-classification.lock.yml | 883 +++++++++++------- .github/workflows/issue-classification.md | 4 +- .github/workflows/issue-triage.lock.yml | 854 ++++++++++------- ...en-code-to-accept-upgrade-changes.lock.yml | 848 ++++++++++------- .github/workflows/java-codegen-fix.lock.yml | 848 ++++++++++------- .github/workflows/publish.yml | 1 + .github/workflows/release-changelog.lock.yml | 845 ++++++++++------- .../workflows/sdk-consistency-review.lock.yml | 855 ++++++++++------- .github/workflows/verify-compiled.yml | 4 +- dotnet/test/E2E/PendingWorkResumeE2ETests.cs | 2 + dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs | 15 +- dotnet/test/Harness/E2ETestContext.cs | 6 +- nodejs/scripts/package-sdk.ts | 1 + nodejs/scripts/verify-release-packages.ts | 12 + python/e2e/test_multi_client_e2e.py | 24 +- rust/tests/e2e/rpc_shell_edge_cases.rs | 13 +- test/harness/replayingCapiProxy.test.ts | 33 + test/harness/replayingCapiProxy.ts | 2 +- 31 files changed, 5905 insertions(+), 4302 deletions(-) delete mode 100644 .github/workflows/agentics-maintenance.yml diff --git a/.gitattributes b/.gitattributes index 2a92ef0172..8d991b1183 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -.github/workflows/*.lock.yml linguist-generated=true merge=ours +.github/workflows/*.lock.yml linguist-generated=true # Cross-platform tools rewrite these files, so keep their output deterministic. java/**/*.java text eol=lf diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 4be7dfbd5c..23d2b6242f 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -20,15 +20,10 @@ "version": "v7.0.1", "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" }, - "github/gh-aw-actions/setup-cli@v0.83.1": { - "repo": "github/gh-aw-actions/setup-cli", - "version": "v0.83.1", - "sha": "8bdba8075360648fe6802302a5b4e016361dc6ac" - }, - "github/gh-aw-actions/setup@v0.83.1": { + "github/gh-aw-actions/setup@v0.88.2": { "repo": "github/gh-aw-actions/setup", - "version": "v0.83.1", - "sha": "8bdba8075360648fe6802302a5b4e016361dc6ac" + "version": "v0.88.2", + "sha": "9271a1804551c0dc4fb0085a97979950aa2f8489" } } } diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md index acec3f146c..4c983cc5ac 100644 --- a/.github/skills/agentic-workflows/SKILL.md +++ b/.github/skills/agentic-workflows/SKILL.md @@ -15,6 +15,8 @@ Repository overlay (optional): Read only the files you need: Load these files from `github/gh-aw` (they are not available locally). +- `.github/aw/action-container-substitutions.md` +- `.github/aw/agent-runtime-instructions.md` - `.github/aw/agentic-chat.md` - `.github/aw/agentic-workflows-mcp.md` - `.github/aw/asciicharts.md` @@ -22,6 +24,7 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/charts-trending.md` - `.github/aw/charts.md` - `.github/aw/cli-commands.md` +- `.github/aw/compat.md` - `.github/aw/configure-agentic-engine.md` - `.github/aw/context.md` - `.github/aw/create-agentic-workflow-trigger-details.md` @@ -30,15 +33,24 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/debug-agentic-workflow.md` - `.github/aw/dependabot.md` - `.github/aw/deployment-status.md` +- `.github/aw/designer-mappings.md` - `.github/aw/designer.md` +- `.github/aw/drive-memory.md` +- `.github/aw/enclaves.md` - `.github/aw/evals.md` - `.github/aw/experiments.md` - `.github/aw/github-agentic-workflows.md` +- `.github/aw/github-mcp-server-pagination.md` +- `.github/aw/github-mcp-server-tools.md` - `.github/aw/github-mcp-server.md` - `.github/aw/instructions.md` +- `.github/aw/intent.md` +- `.github/aw/jobs.md` +- `.github/aw/linter-workflows.md` - `.github/aw/llms.md` - `.github/aw/loop.md` - `.github/aw/lsp.md` +- `.github/aw/maintainer.md` - `.github/aw/mcp-clis.md` - `.github/aw/memory-stateful-patterns.md` - `.github/aw/memory.md` @@ -47,7 +59,9 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/network.md` - `.github/aw/optimize-agentic-workflow.md` - `.github/aw/patterns.md` +- `.github/aw/playwright.md` - `.github/aw/pr-reviewer.md` +- `.github/aw/release-workflow.md` - `.github/aw/report.md` - `.github/aw/reuse.md` - `.github/aw/safe-outputs-automation.md` @@ -61,10 +75,13 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/subagents.md` - `.github/aw/syntax-agentic.md` - `.github/aw/syntax-core.md` +- `.github/aw/syntax-engine.md` - `.github/aw/syntax-tools-imports.md` - `.github/aw/syntax.md` - `.github/aw/test-coverage.md` - `.github/aw/test-expression.md` +- `.github/aw/token-optimization-caching-budgets.md` +- `.github/aw/token-optimization-observability.md` - `.github/aw/token-optimization.md` - `.github/aw/triggers.md` - `.github/aw/update-agentic-workflow.md` @@ -90,5 +107,6 @@ After loading the matching workflow prompt or skill, follow it directly: - Choose workflow architecture and patterns: `.github/aw/patterns.md` - Optimize token usage and cost: `.github/aw/token-optimization.md` - Design long-running multi-agent research workflows: `.github/aw/multi-agent-research.md` +- Add skills or agent plugins requested by the user (`skills:` / `plugins:` frontmatter, never on-the-fly installs): `.github/aw/skills.md` When the task involves OTEL, OTLP, traces, observability backends, or telemetry-driven analysis, also read and follow `skills/otel-queries/SKILL.md` after loading the matching workflow prompt or skill. diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml deleted file mode 100644 index 28c4e67caf..0000000000 --- a/.github/workflows/agentics-maintenance.yml +++ /dev/null @@ -1,633 +0,0 @@ -# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# -# To regenerate this workflow, run: -# gh aw compile -# Not all edits will cause changes to this file. -# -# For more information: https://github.github.com/gh-aw/introduction/overview/ -# -# This file defines the generated agentic maintenance workflow for this repository. -# It runs scheduled cleanup for expiring safe outputs and supports manual maintenance operations. -# -# This workflow is generated automatically when workflows use expiring safe outputs -# or when repository maintenance features are enabled in .github/workflows/aw.json. -# -# To disable maintenance workflow generation, set in .github/workflows/aw.json: -# {"maintenance": false} -# -# Agentic maintenance docs: -# https://github.github.com/gh-aw/reference/ephemerals/#manual-maintenance-operations -# -name: Agentic Maintenance - -on: - schedule: - - cron: "37 0 * * *" # Daily (based on minimum expires: 30 days) - workflow_dispatch: - inputs: - operation: - description: 'Optional maintenance operation to run' - required: false - type: choice - default: '' - options: - - '' - - 'disable' - - 'enable' - - 'update' - - 'upgrade' - - 'safe_outputs' - - 'create_labels' - - 'activity_report' - - 'close_agentic_workflows_issues' - - 'clean_cache_memories' - - 'update_pull_request_branches' - - 'validate' - - 'forecast' - run_url: - description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.' - required: false - type: string - default: '' - workflow_call: - inputs: - operation: - description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, update_pull_request_branches, validate, forecast)' - required: false - type: string - default: '' - run_url: - description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.' - required: false - type: string - default: '' - outputs: - operation_completed: - description: 'The maintenance operation that was completed (empty when none ran or a scheduled job ran)' - value: ${{ jobs.run_operation.outputs.operation || inputs.operation }} - applied_run_url: - description: 'The run URL that safe outputs were applied from' - value: ${{ jobs.apply_safe_outputs.outputs.run_url }} - -permissions: {} - -jobs: - close-expired-discussions: - if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} - runs-on: ubuntu-slim - permissions: - discussions: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Close expired discussions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_discussions.cjs'); - await main(); - close-expired-issues: - if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} - runs-on: ubuntu-slim - permissions: - issues: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Close expired issues - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_issues.cjs'); - await main(); - close-expired-pull-requests: - if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} - runs-on: ubuntu-slim - permissions: - pull-requests: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Close expired pull requests - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_pull_requests.cjs'); - await main(); - - cleanup-cache-memory: - if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories') }} - runs-on: ubuntu-slim - permissions: - actions: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Cleanup outdated cache-memory entries - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/cleanup_cache_memory.cjs'); - await main(); - - run_operation: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'update_pull_request_branches' && inputs.operation != 'validate' && inputs.operation != 'forecast' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - actions: write - contents: write - pull-requests: write - outputs: - operation: ${{ steps.record.outputs.operation }} - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Run operation - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_OPERATION: ${{ inputs.operation }} - GH_AW_CMD_PREFIX: gh aw - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/run_operation_update_upgrade.cjs'); - await main(); - - - name: Record outputs - id: record - env: - GH_AW_OPERATION: ${{ inputs.operation }} - run: echo "operation=$GH_AW_OPERATION" >> "$GITHUB_OUTPUT" - - update_pull_request_branches: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'update_pull_request_branches' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - contents: write - pull-requests: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs'); - await main(); - - - name: Update pull request branches - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/update_pull_request_branches.cjs'); - await main(); - - apply_safe_outputs: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'safe_outputs' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - actions: read - contents: write - discussions: write - issues: write - pull-requests: write - outputs: - run_url: ${{ steps.record.outputs.run_url }} - steps: - - name: Checkout actions folder - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - sparse-checkout: | - actions - clean: false - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs'); - await main(); - - - name: Apply Safe Outputs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_RUN_URL: ${{ inputs.run_url }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/apply_safe_outputs_replay.cjs'); - await main(); - - - name: Record outputs - id: record - env: - GH_AW_RUN_URL: ${{ inputs.run_url }} - run: echo "run_url=$GH_AW_RUN_URL" >> "$GITHUB_OUTPUT" - - create_labels: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'create_labels' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Create missing labels - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_CMD_PREFIX: gh aw - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/create_labels.cjs'); - await main(); - - activity_report: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity_report' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - timeout-minutes: 120 - permissions: - actions: read - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Restore activity report logs cache - id: activity_report_logs_cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ./.cache/gh-aw/activity-report-logs - key: ${{ runner.os }}-activity-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} - restore-keys: | - ${{ runner.os }}-activity-report-logs-${{ github.repository }}- - ${{ runner.os }}-activity-report-logs- - - name: Download activity report logs - timeout-minutes: 20 - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_CMD_PREFIX: gh aw - run: | - ${GH_AW_CMD_PREFIX} logs \ - --repo "$GITHUB_REPOSITORY" \ - --start-date -1w \ - --count 500 \ - --output ./.cache/gh-aw/activity-report-logs \ - --format markdown \ - --report-file ./.cache/gh-aw/activity-report-logs/report.md - - - name: Save activity report logs cache - if: ${{ always() }} - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ./.cache/gh-aw/activity-report-logs - key: ${{ steps.activity_report_logs_cache.outputs.cache-primary-key }} - - - name: Generate activity report issue - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('node:fs'); - const reportPath = './.cache/gh-aw/activity-report-logs/report.md'; - if (!fs.existsSync(reportPath)) { - core.warning('Activity report markdown not found at ' + reportPath + '; skipping issue creation.'); - return; - } - let reportBody = ''; - try { - reportBody = fs.readFileSync(reportPath, 'utf8').trim(); - } catch (error) { - core.warning('Failed to read activity report markdown at ' + reportPath + ': ' + error.message); - return; - } - if (!reportBody) { - core.warning('Activity report markdown is empty at ' + reportPath + '; skipping issue creation.'); - return; - } - const repoSlug = context.repo.owner + '/' + context.repo.repo; - const body = [ - '### Agentic workflow activity report', - '', - 'Repository: ' + repoSlug, - 'Generated at: ' + new Date().toISOString(), - '', - reportBody, - ].join('\n'); - const createdIssue = await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: '[aw] agentic status report', - body, - labels: ['agentic-workflows'], - }); - core.info('Created issue #' + createdIssue.data.number + ': ' + createdIssue.data.html_url); - - forecast_report: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'forecast' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - timeout-minutes: 60 - permissions: - actions: read - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Restore forecast report logs cache - id: forecast_report_logs_cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ./.github/aw/logs - key: ${{ runner.os }}-forecast-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} - restore-keys: | - ${{ runner.os }}-forecast-report-logs-${{ github.repository }}- - ${{ runner.os }}-forecast-report-logs- - - - name: Generate forecast report - id: generate_forecast_report - timeout-minutes: 30 - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEBUG: "*" - GH_AW_CMD_PREFIX: gh aw - run: | - mkdir -p ./.cache/gh-aw/forecast - set +e - ${GH_AW_CMD_PREFIX} forecast --repo "$GITHUB_REPOSITORY" --timeout 30 --verbose --json > ./.cache/gh-aw/forecast/report.json - forecast_exit_code=$? - set -e - if [ "${forecast_exit_code}" -eq 124 ]; then - echo '{"outcome":"timeout","message":"Forecast computation timed out after 30 minutes."}' > ./.cache/gh-aw/forecast/error.json - echo "::error::Forecast computation timed out after 30 minutes." - exit 1 - fi - if [ "${forecast_exit_code}" -ne 0 ]; then - echo '{"outcome":"error","message":"Forecast computation failed before producing a report."}' > ./.cache/gh-aw/forecast/error.json - echo "::error::Forecast computation failed with exit code ${forecast_exit_code}." - exit 1 - fi - - - name: Debug forecast logs folder - if: ${{ always() }} - shell: bash - run: | - if [ ! -d ./.github/aw/logs ]; then - echo "Logs directory not found: ./.github/aw/logs" - exit 0 - fi - echo "Files under ./.github/aw/logs:" - find ./.github/aw/logs -type f | sort - - - name: Save forecast report logs cache - if: ${{ always() }} - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ./.github/aw/logs - key: ${{ runner.os }}-forecast-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} - - - name: Generate forecast issue - if: ${{ always() }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - FORECAST_STEP_OUTCOME: ${{ steps.generate_forecast_report.outcome }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/create_forecast_issue.cjs'); - await main(); - - close_agentic_workflows_issues: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'close_agentic_workflows_issues' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - issues: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs'); - await main(); - - - name: Close no-repro agentic-workflows issues - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/close_agentic_workflows_issues.cjs'); - await main(); - - validate_workflows: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'validate' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Validate workflows and file issue on findings - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_CMD_PREFIX: gh aw - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/run_validate_workflows.cjs'); - await main(); diff --git a/.github/workflows/cross-repo-issue-analysis.lock.yml b/.github/workflows/cross-repo-issue-analysis.lock.yml index 510618f041..f5c636eb05 100644 --- a/.github/workflows/cross-repo-issue-analysis.lock.yml +++ b/.github/workflows/cross-repo-issue-analysis.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"16de319b1db6be0d409d3055ca8fa9f619f2c0120dad678248a45dce07b880b4","body_hash":"653dfb46c89df98eca22ddfb802149d6ade32e9a7ad40dbdc51bfb6b0ba1c4a3","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","RUNTIME_TRIAGE_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"16de319b1db6be0d409d3055ca8fa9f619f2c0120dad678248a45dce07b880b4","body_hash":"653dfb46c89df98eca22ddfb802149d6ade32e9a7ad40dbdc51bfb6b0ba1c4a3","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","RUNTIME_TRIAGE_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_labels","create_issue","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -35,22 +35,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "SDK Runtime Triage" on: @@ -73,9 +71,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}" + queue: max run-name: "SDK Runtime Triage" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.cross-repo-issue-analysis + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=SDK%20Runtime%20Triage,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: needs: pre_activation @@ -93,6 +100,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -108,7 +116,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -118,34 +126,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -167,9 +180,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -187,38 +202,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -227,38 +241,47 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Compute current body text id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + const { main } = require(path.join(actionsDir, 'compute_text.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -270,63 +293,22 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' - - GH_AW_PROMPT_41a978c1ce3777a8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' - - Tools: create_issue, add_labels(max:3), missing_tool, missing_data, noop - - GH_AW_PROMPT_41a978c1ce3777a8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_41a978c1ce3777a8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' - - {{#runtime-import .github/workflows/cross-repo-issue-analysis.md}} - GH_AW_PROMPT_41a978c1ce3777a8_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: create_issue, add_labels(max:3), missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/cross-repo-issue-analysis.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} GH_AW_GITHUB_EVENT_ISSUE_TITLE: ${{ github.event.issue.title }} @@ -334,14 +316,16 @@ jobs: GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -357,10 +341,12 @@ jobs: GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -383,16 +369,20 @@ jobs: }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -419,12 +409,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: crossrepoissueanalysis outputs: @@ -438,7 +435,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -446,11 +446,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -459,19 +460,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -507,16 +517,19 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -525,13 +538,15 @@ jobs: GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -543,15 +558,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f9dc8569c195ba44_EOF' - {"add_labels":{"allowed":["runtime","sdk-fix-only","needs-investigation"],"max":3,"target":"triggering"},"create_issue":{"labels":["upstream-from-sdk","ai-triaged"],"max":1,"target-repo":"github/copilot-agent-runtime","title_prefix":"[copilot-sdk] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_f9dc8569c195ba44_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -584,6 +610,7 @@ jobs: "create_issue": { "defaultMax": 1, "fields": { + "blocked_by": {}, "body": { "required": true, "type": "string", @@ -695,9 +722,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -706,6 +735,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} @@ -713,33 +743,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_46604863f3d8e286_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -772,6 +814,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -783,7 +833,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -791,25 +841,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF + GH_AW_MCP_CONFIG_46604863f3d8e286_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -850,18 +907,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -879,14 +951,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(cat:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find:*)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(grep:*)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(head:*)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(ls:*)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tail:*)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(wc:*)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(cat:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find:*)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(grep:*)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(head:*)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(ls:*)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tail:*)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(wc:*)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -894,7 +971,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -915,7 +992,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 20 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -931,7 +1019,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -940,9 +1028,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,RUNTIME_TRIAGE_TOKEN' @@ -966,15 +1056,17 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GH_AW_ALLOWED_GITHUB_REFS: "repo,github/copilot-agent-runtime" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -984,9 +1076,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -994,9 +1088,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -1010,9 +1106,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1020,16 +1118,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1046,6 +1169,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1065,10 +1190,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1085,7 +1210,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1094,15 +1219,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1110,42 +1236,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1159,6 +1272,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1181,9 +1296,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1213,7 +1330,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/cross-repo-issue-analysis.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1221,9 +1338,11 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1238,9 +1357,11 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1253,9 +1374,11 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1268,9 +1391,11 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1283,7 +1408,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1297,6 +1422,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1312,9 +1441,30 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "SDK Runtime Triage" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/cross-repo-issue-analysis.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1326,6 +1476,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1336,7 +1487,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1345,15 +1496,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1361,10 +1519,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1373,7 +1533,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1397,21 +1557,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1419,82 +1565,52 @@ jobs: WORKFLOW_NAME: "SDK Runtime Triage" WORKFLOW_DESCRIPTION: "Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1510,58 +1626,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "SDK Runtime Triage" + WORKFLOW_DESCRIPTION: "Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json pre_activation: if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'runtime triage' @@ -1577,15 +1740,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1595,9 +1758,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + const { main } = require(path.join(actionsDir, 'check_membership.cjs')); await main(); safe_outputs: @@ -1608,7 +1773,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1622,7 +1786,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis" @@ -1635,12 +1798,20 @@ jobs: create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1649,15 +1820,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1665,7 +1839,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1681,16 +1857,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1700,4 +1878,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/handle-bug.lock.yml b/.github/workflows/handle-bug.lock.yml index 153e882c44..6d2c916a4f 100644 --- a/.github/workflows/handle-bug.lock.yml +++ b/.github/workflows/handle-bug.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"df4e7ec5b346a28c7de5353cbfb15d329d03eb7092f2ded784ee6602108461e6","body_hash":"376c982b907760113954510ef1aff70d22dcb172c7bb851b2fa3d82121bdbc1c","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c5b0e0eada050c6349c6630c62a7d91425c57a49758a3d8937e851eb3275e5f9","body_hash":"376c982b907760113954510ef1aff70d22dcb172c7bb851b2fa3d82121bdbc1c","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Handles issues classified as bugs by the triage classifier # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Bug Handler" on: @@ -74,7 +72,7 @@ on: description: URL of the first added comment value: ${{ jobs.safe_outputs.outputs.comment_url }} secrets: - COPILOT_GITHUB_TOKEN: + GH_AW_DEFAULT_OTLP_HEADERS: required: false GH_AW_GITHUB_MCP_SERVER_TOKEN: required: false @@ -85,9 +83,18 @@ permissions: {} concurrency: group: "gh-aw-handle-bug-${{ github.run_id }}" + queue: max run-name: "Bug Handler" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.handle-bug + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Bug%20Handler,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -102,6 +109,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -119,7 +127,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -127,10 +135,12 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Resolve host repo for activation checkout id: resolve-host-repo uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -141,9 +151,11 @@ jobs: JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + const { main } = require(path.join(actionsDir, 'resolve_host_repo.cjs')); await main(); - name: Compute artifact prefix id: artifact-prefix @@ -155,27 +167,30 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Bug Handler" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -197,9 +212,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -217,15 +234,16 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Print cross-repo setup guidance @@ -236,7 +254,7 @@ jobs: echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" - name: Checkout .github and .agents folders if: steps.resolve-host-repo.outputs.target_repo == github.repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -244,20 +262,18 @@ jobs: sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -266,27 +282,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -296,75 +319,36 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' - - GH_AW_PROMPT_04d69bd6df5739b0_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' - - Tools: add_comment, add_labels, missing_tool, missing_data, noop - - GH_AW_PROMPT_04d69bd6df5739b0_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_04d69bd6df5739b0_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' - - {{#runtime-import .github/workflows/handle-bug.md}} - GH_AW_PROMPT_04d69bd6df5739b0_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, add_labels, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/handle-bug.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -374,13 +358,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -394,22 +379,25 @@ jobs: GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.artifact-prefix.outputs.prefix }}activation @@ -439,12 +427,19 @@ jobs: concurrency: group: "gh-aw-copilot-handle-bug-${{ inputs.issue_number }}" queue: max + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handlebug outputs: @@ -459,7 +454,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -467,11 +465,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -480,20 +479,29 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -523,16 +531,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -542,7 +553,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -554,8 +567,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -567,15 +580,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_18a2d1564ec59c01_EOF' - {"add_comment":{"max":1,"target":"*"},"add_labels":{"allowed":["bug","enhancement","question","documentation"],"max":1,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_18a2d1564ec59c01_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -598,9 +622,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -608,6 +641,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -704,9 +747,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -715,38 +760,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -782,6 +840,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -793,7 +859,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -801,25 +867,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -831,22 +904,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 20 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -864,14 +955,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -879,7 +975,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -900,7 +996,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 20 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -916,7 +1023,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -925,9 +1032,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -950,14 +1059,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -967,9 +1078,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -977,9 +1090,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -993,9 +1108,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1003,16 +1120,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1031,6 +1173,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1050,10 +1194,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1070,7 +1214,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1079,8 +1223,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1088,7 +1232,8 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1096,42 +1241,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + pattern: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1145,6 +1277,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1167,9 +1301,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1199,7 +1335,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-bug.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1207,9 +1343,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1224,9 +1362,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1239,9 +1379,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1254,9 +1396,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1269,7 +1413,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "handle-bug" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1283,6 +1427,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1298,9 +1446,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Bug Handler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-bug.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1312,6 +1481,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1322,7 +1492,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1331,16 +1501,23 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.agent.outputs.artifact_prefix }}agent + pattern: "{${{ needs.agent.outputs.artifact_prefix }}agent,${{ needs.agent.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1348,10 +1525,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1360,7 +1539,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1384,21 +1563,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1406,82 +1571,52 @@ jobs: WORKFLOW_NAME: "Bug Handler" WORKFLOW_DESCRIPTION: "Handles issues classified as bugs by the triage classifier" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1497,58 +1632,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Bug Handler" + WORKFLOW_DESCRIPTION: "Handles issues classified as bugs by the triage classifier" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ needs.agent.outputs.artifact_prefix }}detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1558,7 +1740,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1572,7 +1753,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-bug" @@ -1585,12 +1765,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1599,16 +1787,19 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1616,7 +1807,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1632,16 +1825,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1651,4 +1846,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/handle-bug.md b/.github/workflows/handle-bug.md index 8d426ce5d6..21f9532bc2 100644 --- a/.github/workflows/handle-bug.md +++ b/.github/workflows/handle-bug.md @@ -18,6 +18,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -62,4 +64,4 @@ Based on your investigation, take **one** of the following actions: **Always leave a comment** explaining your findings, even when confirming the issue is a bug. Include: - What you investigated (which files/code paths you looked at) - What you found (is the behavior intentional or not) -- Why you applied the label you chose +- Why you applied the label you chose \ No newline at end of file diff --git a/.github/workflows/handle-documentation.lock.yml b/.github/workflows/handle-documentation.lock.yml index 0a5e68efe3..ea7cd2121d 100644 --- a/.github/workflows/handle-documentation.lock.yml +++ b/.github/workflows/handle-documentation.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6c3b9dc8d0f7b54d44175db209cda34e37ea8c635d01ee0a5cba13675053f6cd","body_hash":"81c8287f5691cdc10ae8f60c004bb671d9b4942740d73fcc9646e28fbcd8790e","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6624c9bc7f3d51508aa278e0086a6066843a0331d8220f477c10f156f26f7960","body_hash":"81c8287f5691cdc10ae8f60c004bb671d9b4942740d73fcc9646e28fbcd8790e","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Handles issues classified as documentation-related by the triage classifier # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Documentation Handler" on: @@ -74,7 +72,7 @@ on: description: URL of the first added comment value: ${{ jobs.safe_outputs.outputs.comment_url }} secrets: - COPILOT_GITHUB_TOKEN: + GH_AW_DEFAULT_OTLP_HEADERS: required: false GH_AW_GITHUB_MCP_SERVER_TOKEN: required: false @@ -85,9 +83,18 @@ permissions: {} concurrency: group: "gh-aw-handle-documentation-${{ github.run_id }}" + queue: max run-name: "Documentation Handler" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.handle-documentation + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Documentation%20Handler,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -102,6 +109,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -119,7 +127,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -127,10 +135,12 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Resolve host repo for activation checkout id: resolve-host-repo uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -141,9 +151,11 @@ jobs: JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + const { main } = require(path.join(actionsDir, 'resolve_host_repo.cjs')); await main(); - name: Compute artifact prefix id: artifact-prefix @@ -155,27 +167,30 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Documentation Handler" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -197,9 +212,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -217,15 +234,16 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Print cross-repo setup guidance @@ -236,7 +254,7 @@ jobs: echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" - name: Checkout .github and .agents folders if: steps.resolve-host-repo.outputs.target_repo == github.repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -244,20 +262,18 @@ jobs: sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -266,27 +282,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -296,75 +319,36 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' - - GH_AW_PROMPT_b3d8e6ce75517df8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' - - Tools: add_comment, add_labels, missing_tool, missing_data, noop - - GH_AW_PROMPT_b3d8e6ce75517df8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_b3d8e6ce75517df8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' - - {{#runtime-import .github/workflows/handle-documentation.md}} - GH_AW_PROMPT_b3d8e6ce75517df8_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, add_labels, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/handle-documentation.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -374,13 +358,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -394,22 +379,25 @@ jobs: GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.artifact-prefix.outputs.prefix }}activation @@ -439,12 +427,19 @@ jobs: concurrency: group: "gh-aw-copilot-handle-documentation-${{ inputs.issue_number }}" queue: max + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handledocumentation outputs: @@ -459,7 +454,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -467,11 +465,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -480,20 +479,29 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -523,16 +531,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -542,7 +553,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -554,8 +567,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -567,15 +580,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6c1b251bac7b1edb_EOF' - {"add_comment":{"max":1,"target":"*"},"add_labels":{"allowed":["documentation"],"max":1,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_6c1b251bac7b1edb_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -598,9 +622,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -608,6 +641,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -704,9 +747,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -715,38 +760,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -782,6 +840,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -793,7 +859,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -801,25 +867,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -831,22 +904,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 5 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -864,14 +955,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -879,7 +975,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 5 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -900,7 +996,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 5 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -916,7 +1023,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -925,9 +1032,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -950,14 +1059,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -967,9 +1078,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -977,9 +1090,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -993,9 +1108,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1003,16 +1120,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1031,6 +1173,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1050,10 +1194,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1070,7 +1214,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1079,8 +1223,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1088,7 +1232,8 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1096,42 +1241,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + pattern: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1145,6 +1277,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1167,9 +1301,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1199,7 +1335,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-documentation.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1207,9 +1343,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1224,9 +1362,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1239,9 +1379,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1254,9 +1396,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1269,7 +1413,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "handle-documentation" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1283,6 +1427,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1298,9 +1446,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Documentation Handler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-documentation.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1312,6 +1481,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1322,7 +1492,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1331,16 +1501,23 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.agent.outputs.artifact_prefix }}agent + pattern: "{${{ needs.agent.outputs.artifact_prefix }}agent,${{ needs.agent.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1348,10 +1525,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1360,7 +1539,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1384,21 +1563,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1406,82 +1571,52 @@ jobs: WORKFLOW_NAME: "Documentation Handler" WORKFLOW_DESCRIPTION: "Handles issues classified as documentation-related by the triage classifier" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1497,58 +1632,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Documentation Handler" + WORKFLOW_DESCRIPTION: "Handles issues classified as documentation-related by the triage classifier" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ needs.agent.outputs.artifact_prefix }}detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1558,7 +1740,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1572,7 +1753,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-documentation" @@ -1585,12 +1765,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1599,16 +1787,19 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1616,7 +1807,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1632,16 +1825,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1651,4 +1846,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/handle-documentation.md b/.github/workflows/handle-documentation.md index 12449a85ca..543d7f71d5 100644 --- a/.github/workflows/handle-documentation.md +++ b/.github/workflows/handle-documentation.md @@ -18,6 +18,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -44,4 +46,4 @@ You are an AI agent that handles issues classified as documentation-related in t 4. Leave a comment that includes: - A summary of the documentation gap (what is missing, incorrect, or unclear) - Which documentation pages, files, or sections are affected - - A brief description of what content should be added or improved to resolve the issue + - A brief description of what content should be added or improved to resolve the issue \ No newline at end of file diff --git a/.github/workflows/handle-enhancement.lock.yml b/.github/workflows/handle-enhancement.lock.yml index 5943203871..b87f3c22a3 100644 --- a/.github/workflows/handle-enhancement.lock.yml +++ b/.github/workflows/handle-enhancement.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f194730258943dec72121a119dba066ff5fee588d79f69fddddd4ca29b56ffd4","body_hash":"624219976b9b7078c6bb11c4177925478cfd8316fe8de535a581bdd176eda825","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c7efbd4a51f5a1e6f8b189c420a07337135cdb5f25290588d167dc9b4ae1c117","body_hash":"624219976b9b7078c6bb11c4177925478cfd8316fe8de535a581bdd176eda825","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Handles issues classified as enhancements by the triage classifier # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Enhancement Handler" on: @@ -74,7 +72,7 @@ on: description: URL of the first added comment value: ${{ jobs.safe_outputs.outputs.comment_url }} secrets: - COPILOT_GITHUB_TOKEN: + GH_AW_DEFAULT_OTLP_HEADERS: required: false GH_AW_GITHUB_MCP_SERVER_TOKEN: required: false @@ -85,9 +83,18 @@ permissions: {} concurrency: group: "gh-aw-handle-enhancement-${{ github.run_id }}" + queue: max run-name: "Enhancement Handler" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.handle-enhancement + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Enhancement%20Handler,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -102,6 +109,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -119,7 +127,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -127,10 +135,12 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Resolve host repo for activation checkout id: resolve-host-repo uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -141,9 +151,11 @@ jobs: JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + const { main } = require(path.join(actionsDir, 'resolve_host_repo.cjs')); await main(); - name: Compute artifact prefix id: artifact-prefix @@ -155,27 +167,30 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Enhancement Handler" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -197,9 +212,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -217,15 +234,16 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Print cross-repo setup guidance @@ -236,7 +254,7 @@ jobs: echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" - name: Checkout .github and .agents folders if: steps.resolve-host-repo.outputs.target_repo == github.repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -244,20 +262,18 @@ jobs: sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -266,27 +282,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -296,75 +319,36 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' - - GH_AW_PROMPT_e59da2f8e25b61b4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' - - Tools: add_comment, add_labels, missing_tool, missing_data, noop - - GH_AW_PROMPT_e59da2f8e25b61b4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_e59da2f8e25b61b4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' - - {{#runtime-import .github/workflows/handle-enhancement.md}} - GH_AW_PROMPT_e59da2f8e25b61b4_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, add_labels, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/handle-enhancement.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -374,13 +358,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -394,22 +379,25 @@ jobs: GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.artifact-prefix.outputs.prefix }}activation @@ -439,12 +427,19 @@ jobs: concurrency: group: "gh-aw-copilot-handle-enhancement-${{ inputs.issue_number }}" queue: max + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handleenhancement outputs: @@ -459,7 +454,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -467,11 +465,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -480,20 +479,29 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -523,16 +531,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -542,7 +553,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -554,8 +567,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -567,15 +580,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_a36bb21781ffc2fb_EOF' - {"add_comment":{"max":1,"target":"*"},"add_labels":{"allowed":["enhancement"],"max":1,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_a36bb21781ffc2fb_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"enhancement\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -598,9 +622,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -608,6 +641,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -704,9 +747,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -715,38 +760,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -782,6 +840,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -793,7 +859,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -801,25 +867,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -831,22 +904,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 5 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -864,14 +955,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -879,7 +975,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 5 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -900,7 +996,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 5 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -916,7 +1023,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -925,9 +1032,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -950,14 +1059,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -967,9 +1078,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -977,9 +1090,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -993,9 +1108,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1003,16 +1120,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1031,6 +1173,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1050,10 +1194,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1070,7 +1214,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1079,8 +1223,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1088,7 +1232,8 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1096,42 +1241,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + pattern: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1145,6 +1277,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1167,9 +1301,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1199,7 +1335,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-enhancement.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1207,9 +1343,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1224,9 +1362,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1239,9 +1379,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1254,9 +1396,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1269,7 +1413,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "handle-enhancement" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1283,6 +1427,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1298,9 +1446,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Enhancement Handler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-enhancement.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1312,6 +1481,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1322,7 +1492,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1331,16 +1501,23 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.agent.outputs.artifact_prefix }}agent + pattern: "{${{ needs.agent.outputs.artifact_prefix }}agent,${{ needs.agent.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1348,10 +1525,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1360,7 +1539,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1384,21 +1563,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1406,82 +1571,52 @@ jobs: WORKFLOW_NAME: "Enhancement Handler" WORKFLOW_DESCRIPTION: "Handles issues classified as enhancements by the triage classifier" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1497,58 +1632,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Enhancement Handler" + WORKFLOW_DESCRIPTION: "Handles issues classified as enhancements by the triage classifier" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ needs.agent.outputs.artifact_prefix }}detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1558,7 +1740,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1572,7 +1753,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-enhancement" @@ -1585,12 +1765,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1599,16 +1787,19 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1616,7 +1807,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1632,16 +1825,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"enhancement\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"enhancement\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1651,4 +1846,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/handle-enhancement.md b/.github/workflows/handle-enhancement.md index 9043c181c1..b7c06c1ba9 100644 --- a/.github/workflows/handle-enhancement.md +++ b/.github/workflows/handle-enhancement.md @@ -18,6 +18,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -34,4 +36,4 @@ timeout-minutes: 5 # Enhancement Handler -Add the `enhancement` label to issue #${{ inputs.issue_number }}. +Add the `enhancement` label to issue #${{ inputs.issue_number }}. \ No newline at end of file diff --git a/.github/workflows/handle-question.lock.yml b/.github/workflows/handle-question.lock.yml index 0093edce0e..2c319aa805 100644 --- a/.github/workflows/handle-question.lock.yml +++ b/.github/workflows/handle-question.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"92158ba4f7e373cb65457b060c7645569b32c756c13c025837491f6809cf694f","body_hash":"1bdd19aae2095beb6e3fcf7af755cd102d424de3a8727ef6e4674815950c7e8b","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d091d519856c3b08a2355cc9d035c3a6e249aa088c107cdfb5b00e8e9aea67bc","body_hash":"1bdd19aae2095beb6e3fcf7af755cd102d424de3a8727ef6e4674815950c7e8b","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Handles issues classified as questions by the triage classifier # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Question Handler" on: @@ -74,7 +72,7 @@ on: description: URL of the first added comment value: ${{ jobs.safe_outputs.outputs.comment_url }} secrets: - COPILOT_GITHUB_TOKEN: + GH_AW_DEFAULT_OTLP_HEADERS: required: false GH_AW_GITHUB_MCP_SERVER_TOKEN: required: false @@ -85,9 +83,18 @@ permissions: {} concurrency: group: "gh-aw-handle-question-${{ github.run_id }}" + queue: max run-name: "Question Handler" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.handle-question + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Question%20Handler,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -102,6 +109,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -119,7 +127,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -127,10 +135,12 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Resolve host repo for activation checkout id: resolve-host-repo uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -141,9 +151,11 @@ jobs: JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + const { main } = require(path.join(actionsDir, 'resolve_host_repo.cjs')); await main(); - name: Compute artifact prefix id: artifact-prefix @@ -155,27 +167,30 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Question Handler" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -197,9 +212,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -217,15 +234,16 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Print cross-repo setup guidance @@ -236,7 +254,7 @@ jobs: echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" - name: Checkout .github and .agents folders if: steps.resolve-host-repo.outputs.target_repo == github.repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -244,20 +262,18 @@ jobs: sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -266,27 +282,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -296,75 +319,36 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' - - GH_AW_PROMPT_a6cfd5b92b97c528_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' - - Tools: add_comment, add_labels, missing_tool, missing_data, noop - - GH_AW_PROMPT_a6cfd5b92b97c528_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_a6cfd5b92b97c528_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' - - {{#runtime-import .github/workflows/handle-question.md}} - GH_AW_PROMPT_a6cfd5b92b97c528_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, add_labels, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/handle-question.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -374,13 +358,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -394,22 +379,25 @@ jobs: GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.artifact-prefix.outputs.prefix }}activation @@ -439,12 +427,19 @@ jobs: concurrency: group: "gh-aw-copilot-handle-question-${{ inputs.issue_number }}" queue: max + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handlequestion outputs: @@ -459,7 +454,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -467,11 +465,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -480,20 +479,29 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -523,16 +531,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -542,7 +553,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -554,8 +567,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -567,15 +580,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_564a988b74c338ef_EOF' - {"add_comment":{"max":1,"target":"*"},"add_labels":{"allowed":["question"],"max":1,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_564a988b74c338ef_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"question\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -598,9 +622,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -608,6 +641,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -704,9 +747,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -715,38 +760,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -782,6 +840,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -793,7 +859,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -801,25 +867,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -831,22 +904,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 5 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -864,14 +955,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -879,7 +975,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 5 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -900,7 +996,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 5 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -916,7 +1023,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -925,9 +1032,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -950,14 +1059,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -967,9 +1078,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -977,9 +1090,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -993,9 +1108,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1003,16 +1120,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1031,6 +1173,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1050,10 +1194,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1070,7 +1214,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1079,8 +1223,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1088,7 +1232,8 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1096,42 +1241,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + pattern: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1145,6 +1277,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1167,9 +1301,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1199,7 +1335,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-question.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1207,9 +1343,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1224,9 +1362,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1239,9 +1379,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1254,9 +1396,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1269,7 +1413,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "handle-question" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1283,6 +1427,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1298,9 +1446,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Question Handler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-question.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1312,6 +1481,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1322,7 +1492,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1331,16 +1501,23 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.agent.outputs.artifact_prefix }}agent + pattern: "{${{ needs.agent.outputs.artifact_prefix }}agent,${{ needs.agent.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1348,10 +1525,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1360,7 +1539,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1384,21 +1563,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1406,82 +1571,52 @@ jobs: WORKFLOW_NAME: "Question Handler" WORKFLOW_DESCRIPTION: "Handles issues classified as questions by the triage classifier" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1497,58 +1632,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Question Handler" + WORKFLOW_DESCRIPTION: "Handles issues classified as questions by the triage classifier" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ needs.agent.outputs.artifact_prefix }}detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1558,7 +1740,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1572,7 +1753,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-question" @@ -1585,12 +1765,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1599,16 +1787,19 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1616,7 +1807,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1632,16 +1825,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"question\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"question\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1651,4 +1846,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/handle-question.md b/.github/workflows/handle-question.md index 21a10d468a..eb40378636 100644 --- a/.github/workflows/handle-question.md +++ b/.github/workflows/handle-question.md @@ -18,6 +18,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -34,4 +36,4 @@ timeout-minutes: 5 # Question Handler -Add the `question` label to issue #${{ inputs.issue_number }}. +Add the `question` label to issue #${{ inputs.issue_number }}. \ No newline at end of file diff --git a/.github/workflows/issue-classification.lock.yml b/.github/workflows/issue-classification.lock.yml index 041fb94b73..c0020170f3 100644 --- a/.github/workflows/issue-classification.lock.yml +++ b/.github/workflows/issue-classification.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"797f7487a67c2fa4465cb3fd31e17c9f0620bb232b2a4fb693c62cb76d5d5a36","body_hash":"8e7ac9b7bb6ab07630a10a4a016108ba59f70feadf82a7391ca0ba5504e14bff","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c68d65ddf05c945729544eaf3436f32818047cc55a9f8699db993f5b3db744bc","body_hash":"8e7ac9b7bb6ab07630a10a4a016108ba59f70feadf82a7391ca0ba5504e14bff","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","call_workflow","handle_bug","handle_documentation","handle_enhancement","handle_question","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Classifies newly opened issues and delegates to type-specific handler workflows # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Issue Classification Agent" on: @@ -73,9 +71,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}" + queue: max run-name: "Issue Classification Agent" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.issue-classification + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Issue%20Classification%20Agent,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -90,6 +97,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -105,7 +113,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -113,34 +121,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -162,9 +175,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -182,38 +197,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -222,38 +236,47 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Compute current body text id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + const { main } = require(path.join(actionsDir, 'compute_text.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -265,63 +288,22 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' - - GH_AW_PROMPT_2b9644ded951c90e_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' - - Tools: add_comment, call_workflow, missing_tool, missing_data, noop - - GH_AW_PROMPT_2b9644ded951c90e_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_2b9644ded951c90e_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' - - {{#runtime-import .github/workflows/issue-classification.md}} - GH_AW_PROMPT_2b9644ded951c90e_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, handle_bug, handle_enhancement, handle_question, handle_documentation, missing_tool, missing_data, noop\nShared budgets: call-workflow [handle_bug, handle_enhancement, handle_question, handle_documentation](max:1 total)\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/issue-classification.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} GH_AW_GITHUB_EVENT_ISSUE_TITLE: ${{ github.event.issue.title }} @@ -329,14 +311,16 @@ jobs: GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -348,13 +332,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -370,22 +355,25 @@ jobs: GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -412,12 +400,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: issueclassification outputs: @@ -431,7 +426,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -439,11 +437,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -452,19 +451,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -494,16 +502,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -513,7 +524,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -525,8 +538,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -538,15 +551,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_3e3303377640f8c6_EOF' - {"add_comment":{"max":1,"target":"triggering"},"call_workflow":{"max":1,"workflow_files":{"handle-bug":"./.github/workflows/handle-bug.lock.yml","handle-documentation":"./.github/workflows/handle-documentation.lock.yml","handle-enhancement":"./.github/workflows/handle-enhancement.lock.yml","handle-question":"./.github/workflows/handle-question.lock.yml"},"workflows":["handle-bug","handle-enhancement","handle-question","handle-documentation"]},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_3e3303377640f8c6_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"triggering\"},\"call_workflow\":{\"max\":1,\"workflow_files\":{\"handle-bug\":\"./.github/workflows/handle-bug.lock.yml\",\"handle-documentation\":\"./.github/workflows/handle-documentation.lock.yml\",\"handle-enhancement\":\"./.github/workflows/handle-enhancement.lock.yml\",\"handle-question\":\"./.github/workflows/handle-question.lock.yml\"},\"workflows\":[\"handle-bug\",\"handle-enhancement\",\"handle-question\",\"handle-documentation\"]},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -677,9 +701,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -687,6 +720,33 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + } + } + }, + "call_workflow": { + "defaultMax": 1, + "fields": { + "inputs": { + "type": "object" + }, + "workflow_name": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256, + "minLength": 1, + "pattern": ".*\\S.*", + "patternError": "must not be empty" } } }, @@ -767,9 +827,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -778,38 +840,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -845,6 +920,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -856,7 +939,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -864,25 +947,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -894,22 +984,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 10 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -927,14 +1035,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -942,7 +1055,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 10 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -963,7 +1076,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 10 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -979,7 +1103,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -988,9 +1112,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -1013,14 +1139,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -1030,9 +1158,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -1040,9 +1170,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -1056,9 +1188,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1066,16 +1200,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1094,6 +1253,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1121,7 +1282,7 @@ jobs: issue_number: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_DEFAULT_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} @@ -1142,7 +1303,7 @@ jobs: issue_number: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_DEFAULT_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} @@ -1163,7 +1324,7 @@ jobs: issue_number: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_DEFAULT_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} @@ -1184,7 +1345,7 @@ jobs: issue_number: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_DEFAULT_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} @@ -1201,10 +1362,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1221,7 +1382,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1230,15 +1391,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1246,42 +1408,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1295,6 +1444,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1317,9 +1468,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1349,7 +1502,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-classification.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1357,9 +1510,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1374,9 +1529,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1389,9 +1546,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1404,9 +1563,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1419,7 +1580,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "issue-classification" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1433,6 +1594,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1448,9 +1613,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Issue Classification Agent" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-classification.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1462,6 +1648,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1472,7 +1659,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1481,15 +1668,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1497,10 +1691,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1509,7 +1705,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1533,21 +1729,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1555,82 +1737,52 @@ jobs: WORKFLOW_NAME: "Issue Classification Agent" WORKFLOW_DESCRIPTION: "Classifies newly opened issues and delegates to type-specific handler workflows" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1646,58 +1798,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Issue Classification Agent" + WORKFLOW_DESCRIPTION: "Classifies newly opened issues and delegates to type-specific handler workflows" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1707,7 +1906,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1721,7 +1919,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "issue-classification" @@ -1736,12 +1933,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1750,15 +1955,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1766,7 +1974,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1782,16 +1992,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"triggering\"},\"call_workflow\":{\"max\":1,\"workflow_files\":{\"handle-bug\":\"./.github/workflows/handle-bug.lock.yml\",\"handle-documentation\":\"./.github/workflows/handle-documentation.lock.yml\",\"handle-enhancement\":\"./.github/workflows/handle-enhancement.lock.yml\",\"handle-question\":\"./.github/workflows/handle-question.lock.yml\"},\"workflows\":[\"handle-bug\",\"handle-enhancement\",\"handle-question\",\"handle-documentation\"]},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"triggering\"},\"call_workflow\":{\"max\":1,\"workflow_files\":{\"handle-bug\":\"./.github/workflows/handle-bug.lock.yml\",\"handle-documentation\":\"./.github/workflows/handle-documentation.lock.yml\",\"handle-enhancement\":\"./.github/workflows/handle-enhancement.lock.yml\",\"handle-question\":\"./.github/workflows/handle-question.lock.yml\"},\"workflows\":[\"handle-bug\",\"handle-enhancement\",\"handle-question\",\"handle-documentation\"]},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1801,4 +2013,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/issue-classification.md b/.github/workflows/issue-classification.md index b1e3345f91..8b111cd1db 100644 --- a/.github/workflows/issue-classification.md +++ b/.github/workflows/issue-classification.md @@ -16,6 +16,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -123,4 +125,4 @@ Common areas of issues: - Issue number: ${{ github.event.issue.number || inputs.issue_number }} - Issue title: ${{ github.event.issue.title }} -Use the GitHub tools to fetch the full issue details, especially when triggered manually via `workflow_dispatch`. +Use the GitHub tools to fetch the full issue details, especially when triggered manually via `workflow_dispatch`. \ No newline at end of file diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index e24584f26d..e6b1410607 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b343e48e59d56a2461bafa8c08f4f37d4242a56fa662b83c2f0cad16262682e5","body_hash":"30994be7c5c23b102c12a56a325ac313e413a2507dff11d0dc695899379bfbd0","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b343e48e59d56a2461bafa8c08f4f37d4242a56fa662b83c2f0cad16262682e5","body_hash":"30994be7c5c23b102c12a56a325ac313e413a2507dff11d0dc695899379bfbd0","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","close_issue","missing_data","missing_tool","noop","update_issue"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Triages newly opened issues by labeling, acknowledging, requesting clarification, and closing duplicates # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Issue Triage Agent" on: @@ -73,9 +71,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}" + queue: max run-name: "Issue Triage Agent" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.issue-triage + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Issue%20Triage%20Agent,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -90,6 +97,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -105,7 +113,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -113,34 +121,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -162,9 +175,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -182,38 +197,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -222,38 +236,47 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Compute current body text id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + const { main } = require(path.join(actionsDir, 'compute_text.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -265,63 +288,22 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_39930e94844c6d8f_EOF' - - GH_AW_PROMPT_39930e94844c6d8f_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_39930e94844c6d8f_EOF' - - Tools: add_comment(max:2), close_issue, update_issue, add_labels(max:10), missing_tool, missing_data, noop - - GH_AW_PROMPT_39930e94844c6d8f_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_39930e94844c6d8f_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_39930e94844c6d8f_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_39930e94844c6d8f_EOF' - - {{#runtime-import .github/workflows/issue-triage.md}} - GH_AW_PROMPT_39930e94844c6d8f_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment(max:2), close_issue, update_issue, add_labels(max:10), missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/issue-triage.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} GH_AW_GITHUB_EVENT_ISSUE_TITLE: ${{ github.event.issue.title }} @@ -329,14 +311,16 @@ jobs: GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -351,10 +335,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -376,16 +362,20 @@ jobs: }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -412,12 +402,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: issuetriage outputs: @@ -431,7 +428,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -439,11 +439,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -452,19 +453,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -494,16 +504,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -512,13 +525,15 @@ jobs: GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -530,15 +545,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_ee19492f88e4cc0b_EOF' - {"add_comment":{"max":2},"add_labels":{"allowed":["bug","enhancement","question","documentation","sdk/dotnet","sdk/go","sdk/java","sdk/nodejs","sdk/python","priority/high","priority/low","testing","security","needs-info","duplicate"],"issue_intent":true,"max":10,"target":"triggering"},"close_issue":{"issue_intent":true,"max":1,"target":"triggering"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"triggering"}} - GH_AW_SAFE_OUTPUTS_CONFIG_ee19492f88e4cc0b_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\",\"sdk/dotnet\",\"sdk/go\",\"sdk/java\",\"sdk/nodejs\",\"sdk/python\",\"priority/high\",\"priority/low\",\"testing\",\"security\",\"needs-info\",\"duplicate\"],\"issue_intent\":true,\"max\":10,\"target\":\"triggering\"},\"close_issue\":{\"issue_intent\":true,\"max\":1,\"target\":\"triggering\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"triggering\"}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -582,9 +608,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -592,6 +627,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -628,6 +673,9 @@ jobs: ], "x-strip-on-error": true }, + "duplicate_of": { + "issueOrPRNumber": true + }, "issue_number": { "optionalPositiveInteger": true }, @@ -774,9 +822,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -785,6 +835,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -792,33 +843,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_46604863f3d8e286_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -851,6 +914,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -862,7 +933,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -870,25 +941,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF + GH_AW_MCP_CONFIG_46604863f3d8e286_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -904,18 +982,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -933,14 +1026,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -948,7 +1046,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 10 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -969,7 +1067,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 10 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -985,7 +1094,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -994,9 +1103,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -1019,14 +1130,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -1036,9 +1149,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -1046,9 +1161,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -1062,9 +1179,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1072,16 +1191,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1098,6 +1242,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1117,10 +1263,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1137,7 +1283,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1146,15 +1292,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1162,42 +1309,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1211,6 +1345,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1233,9 +1369,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1265,7 +1403,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1273,9 +1411,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1290,9 +1430,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1305,9 +1447,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1320,9 +1464,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1335,7 +1481,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "issue-triage" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1349,6 +1495,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1364,9 +1514,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Issue Triage Agent" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1378,6 +1549,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1388,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1397,15 +1569,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1413,10 +1592,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1425,7 +1606,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1449,21 +1630,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1471,82 +1638,52 @@ jobs: WORKFLOW_NAME: "Issue Triage Agent" WORKFLOW_DESCRIPTION: "Triages newly opened issues by labeling, acknowledging, requesting clarification, and closing duplicates" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1562,58 +1699,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Issue Triage Agent" + WORKFLOW_DESCRIPTION: "Triages newly opened issues by labeling, acknowledging, requesting clarification, and closing duplicates" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1623,7 +1807,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1637,7 +1820,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "issue-triage" @@ -1650,12 +1832,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1664,15 +1854,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1680,7 +1873,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1696,16 +1891,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\",\"sdk/dotnet\",\"sdk/go\",\"sdk/java\",\"sdk/nodejs\",\"sdk/python\",\"priority/high\",\"priority/low\",\"testing\",\"security\",\"needs-info\",\"duplicate\"],\"issue_intent\":true,\"max\":10,\"target\":\"triggering\"},\"close_issue\":{\"issue_intent\":true,\"max\":1,\"target\":\"triggering\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"triggering\"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\",\"sdk/dotnet\",\"sdk/go\",\"sdk/java\",\"sdk/nodejs\",\"sdk/python\",\"priority/high\",\"priority/low\",\"testing\",\"security\",\"needs-info\",\"duplicate\"],\"issue_intent\":true,\"max\":10,\"target\":\"triggering\"},\"close_issue\":{\"issue_intent\":true,\"max\":1,\"target\":\"triggering\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"triggering\"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1715,4 +1912,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml b/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml index e94e0775c0..7b7d524fe7 100644 --- a/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml +++ b/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a5f19a89f89b0693f86ca89ea90e3a633fe19c17bf4d27214fa9124429cdc156","body_hash":"8db09798070cbcba22c42c50a316ae45c8e8c650eeb23c556b44fde8d519550a","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a5f19a89f89b0693f86ca89ea90e3a633fe19c17bf4d27214fa9124429cdc156","body_hash":"8db09798070cbcba22c42c50a316ae45c8e8c650eeb23c556b44fde8d519550a","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_release_by_tag","get_tag","list_branches","list_commits","list_releases","list_starred_repositories","list_tags","search_code","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","missing_data","missing_tool","noop","push_to_pull_request_branch"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -28,8 +28,8 @@ # compiles. Fixes handwritten source and tests only. # # Secrets used: -# - COPILOT_GITHUB_TOKEN # - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -37,22 +37,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Java Handwritten Code Adaptation After CLI Upgrade" on: @@ -76,9 +74,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.ref || github.run_id }}" + queue: max run-name: "Java Handwritten Code Adaptation After CLI Upgrade" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.java-adapt-handwritten-code-to-accept-upgrade-changes + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Java%20Handwritten%20Code%20Adaptation%20After%20CLI%20Upgrade,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -92,6 +99,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -105,7 +113,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -113,34 +121,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -162,9 +175,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -182,38 +197,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -222,27 +236,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"file\":\"safe_outputs_push_to_pr_branch.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -253,79 +274,37 @@ jobs: GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - - GH_AW_PROMPT_67432b380d9d8ebb_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - - Tools: add_comment(max:10), push_to_pull_request_branch, missing_tool, missing_data, noop - GH_AW_PROMPT_67432b380d9d8ebb_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - - GH_AW_PROMPT_67432b380d9d8ebb_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_67432b380d9d8ebb_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - - {{#runtime-import .github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md}} - GH_AW_PROMPT_67432b380d9d8ebb_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment(max:10), push_to_pull_request_branch, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -339,10 +318,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -363,16 +344,20 @@ jobs: }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -398,12 +383,19 @@ jobs: actions: read contents: read copilot-requests: write + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: javaadapthandwrittencodetoacceptupgradechanges outputs: @@ -417,7 +409,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -425,11 +420,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -438,19 +434,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -480,16 +485,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -498,13 +506,15 @@ jobs: GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -516,15 +526,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_fcd407b1cd819e9a_EOF' - {"add_comment":{"max":10,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"if_no_changes":"warn","max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"required_labels":["dependencies","sdk/java"],"target":"*"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_fcd407b1cd819e9a_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"required_labels\":[\"dependencies\",\"sdk/java\"],\"target\":\"*\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -546,9 +567,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -556,6 +586,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -632,6 +672,10 @@ jobs: }, "pull_request_number": { "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 } } }, @@ -655,9 +699,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -666,6 +712,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -673,33 +720,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_58d53a00b5a25078_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_33dc71f92a389606_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -732,6 +791,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -743,7 +810,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -751,25 +818,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_58d53a00b5a25078_EOF + GH_AW_MCP_CONFIG_33dc71f92a389606_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -785,18 +859,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -814,14 +903,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -829,7 +923,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -850,7 +944,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 60 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -866,7 +971,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -875,9 +980,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -900,14 +1007,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -917,9 +1026,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -927,9 +1038,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -943,9 +1056,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -953,16 +1068,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -979,6 +1119,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -998,9 +1140,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: + actions: read contents: write issues: write pull-requests: write @@ -1018,7 +1161,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1027,15 +1170,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1043,42 +1187,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1092,6 +1223,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1114,9 +1247,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1154,9 +1289,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1171,9 +1308,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1186,9 +1325,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1201,9 +1342,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1216,7 +1359,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "java-adapt-handwritten-code-to-accept-upgrade-changes" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1230,6 +1373,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} @@ -1247,9 +1394,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1261,6 +1429,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1271,7 +1440,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1280,15 +1449,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1296,10 +1472,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1308,7 +1486,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1332,21 +1510,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1354,82 +1518,52 @@ jobs: WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" WORKFLOW_DESCRIPTION: "Adapt handwritten Java SDK code to work with regenerated types after a\n@github/copilot version bump. Assumes codegen succeeded and generated code\ncompiles. Fixes handwritten source and tests only." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1445,58 +1579,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" + WORKFLOW_DESCRIPTION: "Adapt handwritten Java SDK code to work with regenerated types after a\n@github/copilot version bump. Assumes codegen succeeded and generated code\ncompiles. Fixes handwritten source and tests only." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1520,7 +1701,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "java-adapt-handwritten-code-to-accept-upgrade-changes" @@ -1533,14 +1713,22 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1549,15 +1737,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1565,7 +1756,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Download patch artifact continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -1574,7 +1767,7 @@ jobs: path: /tmp/gh-aw/ - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1600,17 +1793,19 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"required_labels\":[\"dependencies\",\"sdk/java\"],\"target\":\"*\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"required_labels\":[\"dependencies\",\"sdk/java\"],\"target\":\"*\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1620,4 +1815,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/java-codegen-fix.lock.yml b/.github/workflows/java-codegen-fix.lock.yml index 1d8d28c439..91c4ea1816 100644 --- a/.github/workflows/java-codegen-fix.lock.yml +++ b/.github/workflows/java-codegen-fix.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b0390c9ab9beb0d7e106314299e89e486269ab7f64d8489d5021132d79aa6b9b","body_hash":"63d6ce13a5131b158ddffb10a469aa59e0fdc2278eec4d8de7f6763e0b6f2ea2","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b0390c9ab9beb0d7e106314299e89e486269ab7f64d8489d5021132d79aa6b9b","body_hash":"63d6ce13a5131b158ddffb10a469aa59e0fdc2278eec4d8de7f6763e0b6f2ea2","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_release_by_tag","get_tag","list_branches","list_commits","list_releases","list_starred_repositories","list_tags","search_code","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","missing_data","missing_tool","noop","push_to_pull_request_branch"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -27,8 +27,8 @@ # mvn verify fails after code generation changes. # # Secrets used: -# - COPILOT_GITHUB_TOKEN # - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -36,22 +36,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Java Codegen Agentic Fix" on: @@ -79,9 +77,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.ref || github.run_id }}" + queue: max run-name: "Java Codegen Agentic Fix" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.java-codegen-fix + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Java%20Codegen%20Agentic%20Fix,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -95,6 +102,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -108,7 +116,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -116,34 +124,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -165,9 +178,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -185,38 +200,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -225,27 +239,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"file\":\"safe_outputs_push_to_pr_branch.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -257,80 +278,38 @@ jobs: GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_ERROR_SUMMARY: ${{ inputs.error_summary }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - - GH_AW_PROMPT_7834a0b5f08e9149_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - - Tools: add_comment(max:5), push_to_pull_request_branch, missing_tool, missing_data, noop - GH_AW_PROMPT_7834a0b5f08e9149_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - - GH_AW_PROMPT_7834a0b5f08e9149_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_7834a0b5f08e9149_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - - {{#runtime-import .github/workflows/java-codegen-fix.md}} - GH_AW_PROMPT_7834a0b5f08e9149_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment(max:5), push_to_pull_request_branch, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/java-codegen-fix.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_ERROR_SUMMARY: ${{ inputs.error_summary }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -345,10 +324,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -370,16 +351,20 @@ jobs: }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -405,12 +390,19 @@ jobs: actions: read contents: read copilot-requests: write + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: javacodegenfix outputs: @@ -424,7 +416,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -432,11 +427,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -445,19 +441,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -487,16 +492,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -505,13 +513,15 @@ jobs: GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -523,15 +533,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_cf285131e299ca5f_EOF' - {"add_comment":{"max":5,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"if_no_changes":"warn","max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"required_labels":["dependencies"],"target":"*"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_cf285131e299ca5f_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"required_labels\":[\"dependencies\"],\"target\":\"*\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -553,9 +574,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -563,6 +593,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -639,6 +679,10 @@ jobs: }, "pull_request_number": { "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 } } }, @@ -662,9 +706,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -673,6 +719,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -680,33 +727,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_58d53a00b5a25078_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_33dc71f92a389606_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -739,6 +798,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -750,7 +817,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -758,25 +825,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_58d53a00b5a25078_EOF + GH_AW_MCP_CONFIG_33dc71f92a389606_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -792,18 +866,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -821,14 +910,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -836,7 +930,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -857,7 +951,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 60 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -873,7 +978,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -882,9 +987,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -907,14 +1014,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -924,9 +1033,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -934,9 +1045,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -950,9 +1063,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -960,16 +1075,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -986,6 +1126,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1005,9 +1147,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: + actions: read contents: write issues: write pull-requests: write @@ -1025,7 +1168,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1034,15 +1177,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1050,42 +1194,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1099,6 +1230,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1121,9 +1254,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1161,9 +1296,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1178,9 +1315,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1193,9 +1332,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1208,9 +1349,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1223,7 +1366,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "java-codegen-fix" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1237,6 +1380,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} @@ -1254,9 +1401,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Java Codegen Agentic Fix" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/java-codegen-fix.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1268,6 +1436,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1278,7 +1447,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1287,15 +1456,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1303,10 +1479,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1315,7 +1493,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1339,21 +1517,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1361,82 +1525,52 @@ jobs: WORKFLOW_NAME: "Java Codegen Agentic Fix" WORKFLOW_DESCRIPTION: "Agentic fix for Java codegen-related build/test failures. Invoked when\nmvn verify fails after code generation changes." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1452,58 +1586,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Java Codegen Agentic Fix" + WORKFLOW_DESCRIPTION: "Agentic fix for Java codegen-related build/test failures. Invoked when\nmvn verify fails after code generation changes." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1527,7 +1708,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "java-codegen-fix" @@ -1540,14 +1720,22 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1556,15 +1744,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1572,7 +1763,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Download patch artifact continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -1581,7 +1774,7 @@ jobs: path: /tmp/gh-aw/ - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1607,17 +1800,19 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"required_labels\":[\"dependencies\"],\"target\":\"*\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"required_labels\":[\"dependencies\"],\"target\":\"*\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1627,4 +1822,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d1961d4a97..5e1d277259 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -114,6 +114,7 @@ jobs: echo "::error::Expected nine Node.js package tarballs, found $TARBALL_COUNT." exit 1 fi + npm run verify:release-packages - name: Upload artifact uses: actions/upload-artifact@v7.0.0 with: diff --git a/.github/workflows/release-changelog.lock.yml b/.github/workflows/release-changelog.lock.yml index 23b19d9b82..558cdc2e27 100644 --- a/.github/workflows/release-changelog.lock.yml +++ b/.github/workflows/release-changelog.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a4a0859e0103be270433c7fe1926a346c46271b4b530b2c08fa5d606d0ab75c4","body_hash":"490b25b529910b1b087df624fd59eaef52e142e84b9503ca1cf87631f4c36b53","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a4a0859e0103be270433c7fe1926a346c46271b4b530b2c08fa5d606d0ab75c4","body_hash":"490b25b529910b1b087df624fd59eaef52e142e84b9503ca1cf87631f4c36b53","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["create_pull_request","missing_data","missing_tool","noop","update_release"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,8 +26,8 @@ # Generates release notes from merged PRs/commits. Triggered by the publish workflow or manually via workflow_dispatch. # # Secrets used: -# - COPILOT_GITHUB_TOKEN # - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -35,22 +35,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Release Changelog Generator" on: @@ -70,9 +68,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" + queue: max run-name: "Release Changelog Generator" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.release-changelog + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Release%20Changelog%20Generator,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -86,6 +93,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -99,7 +107,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -107,34 +115,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -156,9 +169,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -176,38 +191,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -216,27 +230,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"file\":\"safe_outputs_create_pull_request.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -246,79 +267,37 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' - - GH_AW_PROMPT_c642707f673b9ac4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' - - Tools: create_pull_request, update_release, missing_tool, missing_data, noop - GH_AW_PROMPT_c642707f673b9ac4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' - - GH_AW_PROMPT_c642707f673b9ac4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_c642707f673b9ac4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' - - {{#runtime-import .github/workflows/release-changelog.md}} - GH_AW_PROMPT_c642707f673b9ac4_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: create_pull_request, update_release, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/release-changelog.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_GITHUB_EVENT_INPUTS_TAG: ${{ github.event.inputs.tag }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -331,10 +310,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -354,16 +335,20 @@ jobs: }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -391,12 +376,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: releasechangelog outputs: @@ -410,7 +402,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -418,11 +413,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -431,19 +427,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -473,16 +478,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -491,13 +499,15 @@ jobs: GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -509,15 +519,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_269226b895ff9733_EOF' - {"create_pull_request":{"draft":false,"labels":["automation","changelog"],"max":1,"max_patch_files":100,"max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[changelog] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"update_release":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_269226b895ff9733_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"labels\":[\"automation\",\"changelog\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[changelog] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_release\":{\"max\":1}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -551,6 +572,12 @@ jobs: "sanitize": true, "maxLength": 256 }, + "dependencies": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 256 + }, "draft": { "type": "boolean" }, @@ -564,6 +591,18 @@ jobs: "type": "string", "maxLength": 256 }, + "stack_position": { + "optionalPositiveInteger": true + }, + "stack_root": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + }, "title": { "required": true, "type": "string", @@ -675,9 +714,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -686,6 +727,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -693,33 +735,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_46604863f3d8e286_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -752,6 +806,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -763,7 +825,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -771,25 +833,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF + GH_AW_MCP_CONFIG_46604863f3d8e286_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -805,18 +874,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -834,14 +918,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -849,7 +938,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 15 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -870,7 +959,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 15 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -886,7 +986,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -895,9 +995,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -920,14 +1022,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -937,9 +1041,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -947,9 +1053,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -963,9 +1071,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -973,16 +1083,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -999,6 +1134,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1018,9 +1155,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: + actions: read contents: write issues: write pull-requests: write @@ -1038,7 +1176,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1047,15 +1185,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1063,42 +1202,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1112,6 +1238,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1134,9 +1262,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1166,7 +1296,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/release-changelog.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1174,9 +1304,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1191,9 +1323,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1206,9 +1340,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1221,9 +1357,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1236,7 +1374,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "release-changelog" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1250,6 +1388,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} @@ -1267,9 +1409,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Release Changelog Generator" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/release-changelog.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1281,6 +1444,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1291,7 +1455,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1300,15 +1464,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1316,10 +1487,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1328,7 +1501,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1352,21 +1525,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1374,82 +1533,52 @@ jobs: WORKFLOW_NAME: "Release Changelog Generator" WORKFLOW_DESCRIPTION: "Generates release notes from merged PRs/commits. Triggered by the publish workflow or manually via workflow_dispatch." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1465,58 +1594,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Release Changelog Generator" + WORKFLOW_DESCRIPTION: "Generates release notes from merged PRs/commits. Triggered by the publish workflow or manually via workflow_dispatch." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1540,7 +1716,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "release-changelog" @@ -1553,12 +1728,20 @@ jobs: create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1567,15 +1750,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1583,7 +1769,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Download patch artifact continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -1592,7 +1780,7 @@ jobs: path: /tmp/gh-aw/ - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1618,17 +1806,19 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"labels\":[\"automation\",\"changelog\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[changelog] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{},\"update_release\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"labels\":[\"automation\",\"changelog\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[changelog] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_release\":{\"max\":1}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1638,4 +1828,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/sdk-consistency-review.lock.yml b/.github/workflows/sdk-consistency-review.lock.yml index 4480d40559..cf977b5d53 100644 --- a/.github/workflows/sdk-consistency-review.lock.yml +++ b/.github/workflows/sdk-consistency-review.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"79b4a25722897db4bbdb7fa862f579bd3c37e12d0ce147e46d618ab79bce67c4","body_hash":"cc60c817de34cdb662ae4c091203c67a5ef240ca0165b0cd26a842f03b22614f","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","detection_agent_id":"copilot","detection_agent_model":"${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}],"has_pull_request":true} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"79b4a25722897db4bbdb7fa862f579bd3c37e12d0ce147e46d618ab79bce67c4","body_hash":"cc60c817de34cdb662ae4c091203c67a5ef240ca0165b0cd26a842f03b22614f","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","detection_agent_id":"copilot","detection_agent_model":"${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"has_pull_request":true,"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","create_pull_request_review_comment","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Reviews PRs to ensure features are implemented consistently across all SDK language implementations # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "SDK Consistency Review Agent" on: @@ -88,9 +86,20 @@ concurrency: run-name: "SDK Consistency Review Agent" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.sdk-consistency-review + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=SDK%20Consistency%20Review%20Agent,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id + if: > + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id) && + ((github.event_name != 'pull_request' && github.event_name != 'pull_request_review') || github.event.pull_request.stack == null || + github.event.pull_request.stack.position == github.event.pull_request.stack.size) runs-on: ubuntu-slim permissions: actions: read @@ -103,6 +112,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -118,7 +128,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -126,34 +136,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -175,9 +190,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -195,38 +212,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .gemini - .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -235,38 +251,47 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Compute current body text id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + const { main } = require(path.join(actionsDir, 'compute_text.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -277,77 +302,38 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' - - GH_AW_PROMPT_96d45caa4ffc7593_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' - - Tools: add_comment, create_pull_request_review_comment(max:10), missing_tool, missing_data, noop - - GH_AW_PROMPT_96d45caa4ffc7593_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_96d45caa4ffc7593_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' - - {{#runtime-import .github/workflows/sdk-consistency-review.md}} - GH_AW_PROMPT_96d45caa4ffc7593_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, create_pull_request_review_comment(max:10), missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/sdk-consistency-review.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_EXPR_A0E5D436: ${{ github.event.pull_request.number || inputs.pr_number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -361,10 +347,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -385,16 +373,20 @@ jobs: }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -421,12 +413,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: sdkconsistencyreview outputs: @@ -440,7 +439,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} @@ -448,11 +450,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -461,19 +464,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -503,16 +515,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -521,13 +536,15 @@ jobs: GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -539,15 +556,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_05b64a640c1d5c26_EOF' - {"add_comment":{"hide_older_comments":true,"max":1},"create_pull_request_review_comment":{"max":10,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_05b64a640c1d5c26_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -570,9 +598,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -580,6 +617,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -697,9 +744,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -708,6 +757,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -715,33 +765,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_46604863f3d8e286_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -774,6 +836,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -785,7 +855,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -793,25 +863,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF + GH_AW_MCP_CONFIG_46604863f3d8e286_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -827,18 +904,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -856,8 +948,13 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE @@ -867,12 +964,12 @@ jobs: GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 15 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -893,7 +990,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 15 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -909,7 +1017,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -918,9 +1026,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -943,14 +1053,16 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -960,9 +1072,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -970,9 +1084,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -986,9 +1102,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -996,16 +1114,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1022,6 +1165,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1041,10 +1186,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1061,7 +1206,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1070,15 +1215,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1086,42 +1232,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1135,6 +1268,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1157,9 +1292,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1190,7 +1327,7 @@ jobs: GH_AW_TRACKER_ID: "sdk-consistency-review" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1198,9 +1335,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1216,9 +1355,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1232,9 +1373,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1248,9 +1391,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1264,7 +1409,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "sdk-consistency-review" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1278,6 +1423,10 @@ jobs: GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1293,9 +1442,31 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "SDK Consistency Review Agent" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/sdk-consistency-review.md" + GH_AW_TRACKER_ID: "sdk-consistency-review" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1307,6 +1478,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1317,7 +1489,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1326,15 +1498,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1342,10 +1521,12 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1354,7 +1535,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1378,21 +1559,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1400,83 +1567,53 @@ jobs: WORKFLOW_NAME: "SDK Consistency Review Agent" WORKFLOW_DESCRIPTION: "Reviews PRs to ensure features are implemented consistently across all SDK language implementations" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1492,58 +1629,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "SDK Consistency Review Agent" + WORKFLOW_DESCRIPTION: "Reviews PRs to ensure features are implemented consistently across all SDK language implementations" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1553,7 +1737,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1567,7 +1750,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" - GH_AW_ENGINE_VERSION: "1.0.73" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_TRACKER_ID: "sdk-consistency-review" @@ -1581,12 +1764,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1595,15 +1786,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1611,7 +1805,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1627,16 +1823,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1646,4 +1844,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/verify-compiled.yml b/.github/workflows/verify-compiled.yml index 1a3dbb96fd..3cc5110225 100644 --- a/.github/workflows/verify-compiled.yml +++ b/.github/workflows/verify-compiled.yml @@ -17,9 +17,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install gh-aw CLI - uses: github/gh-aw-actions/setup-cli@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + uses: github/gh-aw-actions/setup-cli@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: - version: v0.83.1 + version: v0.88.2 - name: Recompile workflows # Full-repository compile so the diff check below covers all workflows. run: gh aw compile diff --git a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs index e48dbbbcbc..bc3f4bd672 100644 --- a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs +++ b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs @@ -331,6 +331,8 @@ await Task.WhenAll( Assert.Equal("beta", await originalToolBStarted.Task); await suspendedClient.ForceStopAsync(); + releaseOriginalToolA.TrySetResult("ORIGINAL_A_SHOULD_NOT_WIN"); + releaseOriginalToolB.TrySetResult("ORIGINAL_B_SHOULD_NOT_WIN"); await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); var session2 = await Ctx.ResumeSessionAsync(resumedClient, sessionId, new ResumeSessionConfig diff --git a/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs b/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs index b6036b7b8d..cc78099527 100644 --- a/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs +++ b/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs @@ -164,12 +164,10 @@ public async Task Shell_Exec_With_Large_Stdout_Cleans_Up() var session = await CreateSessionAsync(); var markerPath = Path.Join(Ctx.WorkDir, $"shell-stdout-{Guid.NewGuid():N}.txt"); - // Print a payload large enough to exceed the runtime's 64KB chunk threshold so the - // chunked-output path is executed. We use a single 200KB write so the runtime has to - // emit at least 3 chunks (200KB / 64KB ≈ 4). + // Exceed the runtime's 64KB chunk threshold without flooding slower CI runners. var command = OperatingSystem.IsWindows() - ? $"powershell -NoLogo -NoProfile -Command \"Write-Host ('x' * 204800); Set-Content -LiteralPath '{markerPath}' -Value 'done'\"" - : $"printf '%0.s=' $(seq 1 204800); printf 'done' > '{markerPath}'"; + ? $"powershell -NoLogo -NoProfile -Command \"Write-Host ('x' * 71680); Set-Content -LiteralPath '{markerPath}' -Value 'done'\"" + : $"printf '%0.s=' $(seq 1 71680); printf 'done' > '{markerPath}'"; var result = await session.Rpc.Shell.ExecAsync(command); Assert.False(string.IsNullOrWhiteSpace(result.ProcessId)); @@ -185,10 +183,9 @@ await TestHelper.WaitForConditionAsync( private static async Task AssertProcessMapCleanedUpAsync(CopilotSession session, string processId, string scenario) { // The shell RPC surface exposes kill but not a non-mutating status API. - // Give the runtime's close/exit handler a bounded grace period, then - // probe exactly once; if this returns true, the assertion fails instead - // of letting a polling kill make the test pass by cleaning up itself. - await Task.Delay(TimeSpan.FromSeconds(1)); + // Give slower CI runners enough time to flush output and process the exit + // before the single mutating cleanup probe. + await Task.Delay(TimeSpan.FromSeconds(5)); var killResult = await session.Rpc.Shell.KillAsync(processId); Assert.False(killResult.Killed, $"{scenario} should have already exited and been removed from the runtime's process map."); } diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs index 2554c6a4d1..fcba56e70a 100644 --- a/dotnet/test/Harness/E2ETestContext.cs +++ b/dotnet/test/Harness/E2ETestContext.cs @@ -157,9 +157,7 @@ public string GetLegacyCliPath() private static Lazy GetCachedCliPath(string repoRoot, string option) { - var envPath = option == "--print-path" - ? Environment.GetEnvironmentVariable("COPILOT_CLI_PATH") - : null; + var envPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); var cacheKey = $"{repoRoot}\0{option}\0{envPath}"; return s_preparedCliPaths.GetOrAdd( cacheKey, @@ -171,7 +169,7 @@ private static Lazy GetCachedCliPath(string repoRoot, string option) private static string PrepareCliPath(string repoRoot, string option) { var envPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); - if (option == "--print-path" && !string.IsNullOrEmpty(envPath)) return envPath; + if (!string.IsNullOrEmpty(envPath)) return envPath; var startInfo = new ProcessStartInfo { diff --git a/nodejs/scripts/package-sdk.ts b/nodejs/scripts/package-sdk.ts index fbdd7c6092..cc7f2f8464 100644 --- a/nodejs/scripts/package-sdk.ts +++ b/nodejs/scripts/package-sdk.ts @@ -43,6 +43,7 @@ try { name: packageName, version: sdkVersion, description: `Platform runtime for @github/copilot-sdk (${platform})`, + repository: packageJson.repository, license: "MIT", os: [osName], cpu: [cpu], diff --git a/nodejs/scripts/verify-release-packages.ts b/nodejs/scripts/verify-release-packages.ts index e1b13a3074..0d2f4589d9 100644 --- a/nodejs/scripts/verify-release-packages.ts +++ b/nodejs/scripts/verify-release-packages.ts @@ -11,6 +11,7 @@ interface PackedPackage { manifest: { name: string; version: string; + repository?: string | { type?: string; url?: string }; optionalDependencies?: Record; }; entries: Set; @@ -20,6 +21,12 @@ const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const sourceManifest = JSON.parse( readFileSync(join(nodeRoot, "package.json"), "utf8") ) as PackedPackage["manifest"]; +assert( + typeof sourceManifest.repository === "string" + ? sourceManifest.repository.trim() + : sourceManifest.repository?.url?.trim(), + "Main package is missing repository metadata" +); const expectedRuntimePackages = Object.fromEntries( RUNTIME_PLATFORMS.map((platform) => [getRuntimePackageName(platform), sourceManifest.version]) ); @@ -85,6 +92,11 @@ for (const platform of RUNTIME_PLATFORMS) { const packageName = getRuntimePackageName(platform); const packed = packages.get(packageName); assert(packed, `Missing ${packageName} tarball`); + assert.deepEqual( + packed.manifest.repository, + sourceManifest.repository, + `${packageName} repository metadata does not match the main package` + ); const runtimeName = platform.startsWith("win32") ? "copilot-runtime.exe" : "copilot-runtime"; for (const requiredPath of [ `package/prebuilds/${platform}/${runtimeName}`, diff --git a/python/e2e/test_multi_client_e2e.py b/python/e2e/test_multi_client_e2e.py index 91beb2239e..1938ddfe89 100644 --- a/python/e2e/test_multi_client_e2e.py +++ b/python/e2e/test_multi_client_e2e.py @@ -345,7 +345,16 @@ async def test_one_client_rejects_permission_and_both_see_the_result( client2_completed = wait_for_event( session2, lambda event: event.type.value == "permission.completed" ) - waiters = [client1_requested, client2_requested, client1_completed, client2_completed] + session1_idle = wait_for_event( + session1, lambda event: event.type.value == "session.idle" + ) + waiters = [ + client1_requested, + client2_requested, + client1_completed, + client2_completed, + session1_idle, + ] # Create a file that the agent will try to edit test_file = os.path.join(mctx.work_dir, "protected.txt") @@ -353,12 +362,6 @@ async def test_one_client_rejects_permission_and_both_see_the_result( f.write("protected content") await session1.send("Edit protected.txt and replace 'protected' with 'hacked'.") - await get_final_assistant_message(session1) - - # Verify the file was NOT modified (permission was denied) - with open(test_file) as f: - content = f.read() - assert content == "protected content" # Both clients should have seen permission.requested and permission.completed await asyncio.gather(client1_requested, client2_requested) @@ -367,6 +370,13 @@ async def test_one_client_rejects_permission_and_both_see_the_result( completed_events = await asyncio.gather(client1_completed, client2_completed) for event in completed_events: assert event.data.result.kind == "denied-interactively-by-user" + + assert (await session1_idle).type.value == "session.idle" + + # Verify the file was NOT modified (permission was denied) + with open(test_file) as f: + content = f.read() + assert content == "protected content" finally: for waiter in waiters: if not waiter.done(): diff --git a/rust/tests/e2e/rpc_shell_edge_cases.rs b/rust/tests/e2e/rpc_shell_edge_cases.rs index df5ddb1dc7..cdd692e1ab 100644 --- a/rust/tests/e2e/rpc_shell_edge_cases.rs +++ b/rust/tests/e2e/rpc_shell_edge_cases.rs @@ -308,9 +308,9 @@ async fn wait_for_file_text(path: &Path, expected: &'static str) { async fn wait_for_process_cleanup( session: &github_copilot_sdk::session::Session, process_id: String, - _scenario: &'static str, + scenario: &'static str, ) { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + tokio::time::sleep(std::time::Duration::from_secs(5)).await; let result = session .rpc() .shell() @@ -320,7 +320,10 @@ async fn wait_for_process_cleanup( }) .await .expect("probe process cleanup"); - assert!(!result.killed); + assert!( + !result.killed, + "{scenario} should have already exited and been removed from the runtime process map" + ); } #[cfg(windows)] @@ -402,7 +405,7 @@ fn stderr_command(marker_path: &Path) -> String { #[cfg(windows)] fn large_stdout_command(marker_path: &Path) -> String { format!( - "powershell -NoLogo -NoProfile -Command \"Write-Host ('x' * 204800); Set-Content -LiteralPath '{}' -Value done\"", + "powershell -NoLogo -NoProfile -Command \"Write-Host ('x' * 71680); Set-Content -LiteralPath '{}' -Value done\"", marker_path.display() ) } @@ -410,7 +413,7 @@ fn large_stdout_command(marker_path: &Path) -> String { #[cfg(not(windows))] fn large_stdout_command(marker_path: &Path) -> String { format!( - "sh -c \"python3 - <<'PY'\nprint('x' * 204800)\nPY\nprintf done > '{}'\"", + "sh -c \"python3 - <<'PY'\nprint('x' * 71680)\nPY\nprintf done > '{}'\"", marker_path.display() ) } diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index 2aa535e798..c5ba6195b1 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -577,6 +577,14 @@ Always include PINEAPPLE_COCONUT_42. arguments: '{"command":"sleep 100"}', }, }, + { + id: "tc3", + type: "function", + function: { + name: "bash", + arguments: '{"command":"sleep 100"}', + }, + }, ], }, { @@ -590,6 +598,11 @@ Always include PINEAPPLE_COCONUT_42. tool_call_id: "tc2", content: "", }, + { + role: "tool", + tool_call_id: "tc3", + content: "unknown attachedShellSession handle 9", + }, ], }); const responseBody = JSON.stringify({ @@ -607,6 +620,7 @@ Always include PINEAPPLE_COCONUT_42. expect(toolMessages.map((message) => message.content)).toEqual([ "The execution of this tool, or a previous tool was interrupted.", "The execution of this tool, or a previous tool was interrupted.", + "The execution of this tool, or a previous tool was interrupted.", ]); }); @@ -1056,6 +1070,25 @@ Always include PINEAPPLE_COCONUT_42. .message.content, ).toBe("Ready for another request."); + const unknownHandleResponse = await makeRequest( + proxyUrl, + "/chat/completions", + { + body: { + model: "test-model", + messages: [ + ...messages, + { + role: "tool", + tool_call_id: "runtime-call-id", + content: "unknown attachedShellSession handle 9", + }, + ], + }, + }, + ); + expect(unknownHandleResponse.status).toBe(200); + const meaningfulErrorResponse = await makeRequest( proxyUrl, "/chat/completions", diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index 7da04cabc4..ca7f6d0f3f 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -1556,7 +1556,7 @@ function normalizeAvailableToolNames(result: string): string { function normalizeInterruptedToolResult(result: string): string { return result.replace( - /^(?:Failed to execute `[^`]+` tool(?: with arguments: [\s\S]*?)? due to error: (?:Error: )?Session aborted|)$/, + /^(?:Failed to execute `[^`]+` tool(?: with arguments: [\s\S]*?)? due to error: (?:Error: )?Session aborted||unknown attachedShellSession handle \d+)$/, "The execution of this tool, or a previous tool was interrupted.", ); } From 70fa5d311368d414bbcd156f7ae861bd9e105ccf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Sep 2026 21:52:01 +0000 Subject: [PATCH 47/60] docs: update version references to 1.0.13-preview.6 --- java/README.md | 8 ++++---- java/sdk/jbang-example.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java/README.md b/java/README.md index bb71d7ab86..ac174a569a 100644 --- a/java/README.md +++ b/java/README.md @@ -36,14 +36,14 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. com.github copilot-sdk-java - 1.0.13-preview.5 + 1.0.13-preview.6 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.13-preview.5' +implementation 'com.github:copilot-sdk-java:1.0.13-preview.6' ``` #### Snapshot Builds @@ -62,7 +62,7 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.14-preview.5-SNAPSHOT + 1.0.14-preview.6-SNAPSHOT ``` @@ -71,7 +71,7 @@ Snapshot builds of the next development version are published to Maven Central S Replace `${copilot.sdk.version}` with the latest release from Maven Central. ```groovy -implementation 'com.github:copilot-sdk-java:1.0.14-preview.5-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.14-preview.6-SNAPSHOT' ``` ## In-process mode (experimental) diff --git a/java/sdk/jbang-example.java b/java/sdk/jbang-example.java index a223dd8d20..67772adbd8 100644 --- a/java/sdk/jbang-example.java +++ b/java/sdk/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.13-preview.5 +//DEPS com.github:copilot-sdk-java:1.0.13-preview.6 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; From d6791fcd66cd47d9a711a89d05d6eaf846b58a21 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Sep 2026 21:52:39 +0000 Subject: [PATCH 48/60] [maven-release-plugin] prepare release java/v1.0.13-preview.6 --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 0abe03ad18..0caf294992 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.5-SNAPSHOT + 1.0.13-preview.6 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.6 diff --git a/java/pom.xml b/java/pom.xml index 285cb7bb9a..e6835923b6 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.5-SNAPSHOT + 1.0.13-preview.6 pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.6 diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index aa40b25189..e01e26e5cf 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.5-SNAPSHOT + 1.0.13-preview.6 ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - HEAD + java/v1.0.13-preview.6 From 18fba1444b522c4aae0c1984e259d96e5f02a5cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Sep 2026 21:52:57 +0000 Subject: [PATCH 49/60] Revert "[maven-release-plugin] prepare release java/v1.0.13-preview.6" This reverts commit d6791fcd66cd47d9a711a89d05d6eaf846b58a21. --- java/copilot-native/pom.xml | 4 ++-- java/pom.xml | 4 ++-- java/sdk/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 0caf294992..0abe03ad18 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.6 + 1.0.14-preview.5-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.6 + HEAD diff --git a/java/pom.xml b/java/pom.xml index e6835923b6..285cb7bb9a 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.6 + 1.0.14-preview.5-SNAPSHOT pom GitHub Copilot SDK :: Java :: Parent @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.6 + HEAD diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index e01e26e5cf..aa40b25189 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.13-preview.6 + 1.0.14-preview.5-SNAPSHOT ../pom.xml @@ -24,7 +24,7 @@ scm:git:https://github.com/github/copilot-sdk.git scm:git:https://github.com/github/copilot-sdk.git https://github.com/github/copilot-sdk - java/v1.0.13-preview.6 + HEAD From 81ffc2a77c2454a923f8dae3e74663bc49bfbd9f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Sep 2026 21:52:57 +0000 Subject: [PATCH 50/60] Revert "docs: update version references to 1.0.13-preview.6" This reverts commit 70fa5d311368d414bbcd156f7ae861bd9e105ccf. --- java/README.md | 8 ++++---- java/sdk/jbang-example.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java/README.md b/java/README.md index ac174a569a..bb71d7ab86 100644 --- a/java/README.md +++ b/java/README.md @@ -36,14 +36,14 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. com.github copilot-sdk-java - 1.0.13-preview.6 + 1.0.13-preview.5 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.13-preview.6' +implementation 'com.github:copilot-sdk-java:1.0.13-preview.5' ``` #### Snapshot Builds @@ -62,7 +62,7 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.14-preview.6-SNAPSHOT + 1.0.14-preview.5-SNAPSHOT ``` @@ -71,7 +71,7 @@ Snapshot builds of the next development version are published to Maven Central S Replace `${copilot.sdk.version}` with the latest release from Maven Central. ```groovy -implementation 'com.github:copilot-sdk-java:1.0.14-preview.6-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.14-preview.5-SNAPSHOT' ``` ## In-process mode (experimental) diff --git a/java/sdk/jbang-example.java b/java/sdk/jbang-example.java index 67772adbd8..a223dd8d20 100644 --- a/java/sdk/jbang-example.java +++ b/java/sdk/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.13-preview.6 +//DEPS com.github:copilot-sdk-java:1.0.13-preview.5 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; From 184e504c41048ffe7a60a6bb9ec4931321d38246 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:57:00 -0400 Subject: [PATCH 51/60] Update @github/copilot to 1.0.83-4 (#2507) * Update Copilot CLI to 1.0.83-4 - Updated the Node.js CLI release pin - Re-ran code generators - Formatted generated code * Fix regenerated SDK compatibility Preserve optional-null wire semantics, advertise supported task kinds, and correct OAuth callback ordering across SDKs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Remove generated C# Optional wrapper Restore ordinary nullable generated APIs while the runtime contract is redesigned to avoid presence-sensitive null semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix regenerated SDK test call sites Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Format Java model switch call Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Stabilize Python MCP OAuth tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Stabilize MCP OAuth startup tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Stabilize Node MCP OAuth startup tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Handle superseded MCP OAuth requests in tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Stabilize Rust MCP OAuth startup tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Stephen Toub Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 6 +- dotnet/src/Generated/Rpc.cs | 1319 ++++++++++++++++- dotnet/src/Generated/SessionEvents.cs | 158 ++ dotnet/test/E2E/McpOAuthE2ETests.cs | 46 +- dotnet/test/Unit/GitHubTelemetryTests.cs | 3 + go/client.go | 10 +- go/client_test.go | 33 +- go/internal/e2e/mcp_oauth_e2e_test.go | 7 +- go/rpc/generated_rpc_api_shape_test.go | 3 + go/rpc/generated_rpc_union_test.go | 24 + go/rpc/zrpc.go | 661 ++++++++- go/rpc/zrpc_encoding.go | 155 ++ go/rpc/zsession_encoding.go | 18 + go/rpc/zsession_events.go | 58 + go/types.go | 1 + go/zsession_events.go | 11 + java/pom.xml | 2 +- java/scripts/codegen/package-lock.json | 92 +- java/scripts/codegen/package.json | 2 +- .../AutoTierSwitchFailureReason.java | 39 + .../SessionAutoTierSwitchFailedEvent.java | 45 + .../copilot/generated/SessionEvent.java | 6 + .../SessionMcpServerNeedsReconnectEvent.java | 41 + .../SessionMcpServerRemovedEvent.java | 41 + .../generated/SessionModelChangeEvent.java | 6 +- .../generated/rpc/CapiSessionOptions.java | 2 +- .../generated/rpc/ClientTaskCancelReason.java | 35 + .../copilot/generated/rpc/ConnectParams.java | 3 + .../copilot/generated/rpc/ConnectResult.java | 5 +- .../copilot/generated/rpc/CurrentModel.java | 37 + .../generated/rpc/McpConfigRemoveParams.java | 4 +- .../github/copilot/generated/rpc/Model.java | 3 + .../rpc/ModelSwitchAutoTierStatus.java | 35 + .../copilot/generated/rpc/SandboxConfig.java | 10 + .../rpc/ServerManagedSettingsApi.java | 11 + .../generated/rpc/SessionModelApi.java | 16 + ...SessionModelApplyStartupOverlayResult.java | 4 +- .../rpc/SessionModelGetCurrentResult.java | 10 +- .../rpc/SessionModelSwitchAutoTierParams.java | 34 + .../rpc/SessionModelSwitchAutoTierResult.java | 38 + .../rpc/SessionModelSwitchToParams.java | 2 + .../rpc/SessionModelSwitchToResult.java | 4 +- .../generated/rpc/SessionOpenOptions.java | 2 + .../generated/rpc/SessionTasksApi.java | 32 + .../rpc/SessionTasksRegisterParams.java | 42 + .../rpc/SessionTasksRegisterResult.java | 34 + .../rpc/SessionTasksUpdateParams.java | 36 + .../rpc/SessionTasksUpdateResult.java | 34 + .../rpc/TaskClientExecutionMode.java | 33 + .../copilot/generated/rpc/TaskClientInfo.java | 70 + .../generated/rpc/TaskClientOwner.java | 40 + .../generated/rpc/TaskClientOwnerKind.java | 35 + .../rpc/TaskClientOwnerPresence.java | 35 + .../generated/rpc/TaskClientStatus.java | 43 + .../copilot/generated/rpc/TaskClientType.java | 33 + .../copilot/generated/rpc/TaskKind.java | 37 + .../generated/rpc/TasksCancelParams.java | 38 + .../generated/rpc/TasksCancelResult.java | 30 + .../com/github/copilot/CopilotClient.java | 1 + .../com/github/copilot/CopilotSession.java | 10 +- .../github/copilot/GitHubTelemetryTest.java | 1 + .../copilot/RpcSessionStateExtrasE2ETest.java | 2 +- .../com/github/copilot/RpcWrappersTest.java | 2 +- .../rpc/GeneratedRpcApiCoverageTest.java | 2 +- .../rpc/GeneratedRpcRecordsCoverageTest.java | 18 +- nodejs/package.json | 2 +- nodejs/src/cliVersion.ts | 2 +- nodejs/src/client.ts | 7 +- nodejs/src/generated/rpc.ts | 617 +++++++- nodejs/src/generated/session-events.ts | 139 ++ nodejs/test/client.test.ts | 1 + nodejs/test/e2e/mcp_oauth.e2e.test.ts | 77 +- python/copilot/client.py | 4 +- python/copilot/generated/rpc.py | 1082 +++++++++++++- python/copilot/generated/session_events.py | 100 +- python/e2e/test_mcp_oauth_e2e.py | 57 +- python/test_client.py | 1 + rust/src/generated/api_types.rs | 761 +++++++++- rust/src/generated/rpc.rs | 120 +- rust/src/generated/session_events.rs | 68 + rust/src/lib.rs | 5 + rust/src/session.rs | 1 + rust/tests/e2e/mcp_oauth.rs | 35 +- rust/tests/e2e/rpc_mcp_config.rs | 4 + rust/tests/e2e/rpc_tasks_and_handlers.rs | 2 +- scripts/codegen/go.ts | 21 +- 86 files changed, 6479 insertions(+), 277 deletions(-) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/AutoTierSwitchFailureReason.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoTierSwitchFailedEvent.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerNeedsReconnectEvent.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerRemovedEvent.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ClientTaskCancelReason.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientExecutionMode.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientInfo.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwner.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerKind.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerPresence.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientStatus.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientType.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelResult.java diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index b02958ea97..709121dd75 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -2186,7 +2186,8 @@ [new ConnectHandshakeRequest( // Declare the integrating application's identity so the runtime attributes the // telemetry it emits on this connection to a consistent surface instead // of its own build. Null when the app didn't supply it. - ConnectHandshakeClientInfo.From(_options.ClientInfo))], + ConnectHandshakeClientInfo.From(_options.ClientInfo), + SupportedTaskKinds: [TaskKind.Agent, TaskKind.Client, TaskKind.Shell])], connection.StderrBuffer, cancellationToken); serverVersion = (int)connectResponse.ProtocolVersion; @@ -3198,7 +3199,8 @@ internal record GetSessionMetadataResponse( internal record ConnectHandshakeRequest( string? Token, [property: JsonPropertyName("enableGitHubTelemetryForwarding")] bool? EnableGitHubTelemetryForwarding = null, - [property: JsonPropertyName("clientInfo")] ConnectHandshakeClientInfo? ClientInfo = null); + [property: JsonPropertyName("clientInfo")] ConnectHandshakeClientInfo? ClientInfo = null, + [property: JsonPropertyName("supportedTaskKinds")] IList? SupportedTaskKinds = null); internal record ConnectHandshakeClientInfo( [property: JsonPropertyName("editorName")] string? EditorName = null, diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index c4428b46f8..d65aa4f1b8 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -56,6 +56,10 @@ internal sealed class ConnectResult [JsonPropertyName("protocolVersion")] public long ProtocolVersion { get; set; } + /// Task kinds the server may return to this connection. + [JsonPropertyName("taskKinds")] + public IList? TaskKinds { get; set; } + /// Server package version. [JsonPropertyName("version")] public string Version { get; set; } = string.Empty; @@ -94,6 +98,10 @@ internal sealed class ConnectRequest [JsonPropertyName("enableGitHubTelemetryForwarding")] public bool? EnableGitHubTelemetryForwarding { get; set; } + /// Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. + [JsonPropertyName("supportedTaskKinds")] + public IList? SupportedTaskKinds { get; set; } + /// Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN. [JsonPropertyName("token")] public string? Token { get; set; } @@ -433,6 +441,10 @@ public sealed class Model [JsonPropertyName("infoMessages")] public IList? InfoMessages { get; set; } + /// Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. + [JsonPropertyName("metadata")] + public IDictionary? Metadata { get; set; } + /// Model capability category for grouping in the model picker. [JsonPropertyName("modelPickerCategory")] public ModelPickerCategory? ModelPickerCategory { get; set; } @@ -2231,6 +2243,10 @@ internal sealed class McpConfigUpdateRequest [Experimental(Diagnostics.Experimental)] internal sealed class McpConfigRemoveRequest { + /// OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. + [JsonPropertyName("authClientIdMetadataUrl")] + public string? AuthClientIdMetadataUrl { get; set; } + /// Name of the MCP server to remove. [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] @@ -7357,10 +7373,18 @@ internal sealed class FactoryJournalPutRequest public string SessionId { get; set; } = string.Empty; } -/// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. +/// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. [Experimental(Diagnostics.Experimental)] public sealed class CurrentModel { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + [JsonPropertyName("activatingAutoTier")] + public AutoTier? ActivatingAutoTier { get; set; } + + /// Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Context tier for models that support multiple context-window sizes. [JsonPropertyName("contextTier")] public ContextTier? ContextTier { get; set; } @@ -7369,6 +7393,10 @@ public sealed class CurrentModel [JsonPropertyName("modelId")] public string? ModelId { get; set; } + /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + [JsonPropertyName("pendingAutoTier")] + public AutoTier? PendingAutoTier { get; set; } + /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } @@ -7424,6 +7452,10 @@ public sealed class ModelSwitchToResult [JsonPropertyName("modelId")] public string? ModelId { get; set; } + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + [JsonPropertyName("modelState")] + public CurrentModel? ModelState { get; set; } + /// Persistence failure encountered after applying the model switch. [JsonPropertyName("persistenceError")] public string? PersistenceError { get; set; } @@ -7548,6 +7580,10 @@ public sealed class ModelPickerPersistenceRequest [Experimental(Diagnostics.Experimental)] internal sealed class ModelSwitchToRequest { + /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. [JsonPropertyName("compactionDecision")] public string? CompactionDecision { get; set; } @@ -7609,6 +7645,48 @@ internal sealed class ModelSwitchToRequest public Verbosity? Verbosity { get; set; } } +/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +[Experimental(Diagnostics.Experimental)] +public sealed class ModelSwitchAutoTierResult +{ + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + [JsonPropertyName("activatingAutoTier")] + public AutoTier? ActivatingAutoTier { get; set; } + + /// Auto preference currently committed for the session. + [JsonPropertyName("effectiveAutoTier")] + public AutoTier? EffectiveAutoTier { get; set; } + + /// Latest unclaimed Auto preference waiting for a future user turn. + [JsonPropertyName("pendingAutoTier")] + public AutoTier? PendingAutoTier { get; set; } + + /// Immediate request status. `pending` means accepted but not committed. + [JsonPropertyName("status")] + public ModelSwitchAutoTierStatus Status { get; set; } + + /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + [JsonPropertyName("supersededAutoTier")] + public AutoTier? SupersededAutoTier { get; set; } +} + +/// An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. +[Experimental(Diagnostics.Experimental)] +internal sealed class ModelSwitchAutoTierRequest +{ + /// Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + [JsonPropertyName("source")] + public ModelChangeSource? Source { get; set; } +} + /// Managed, repository, and CLI model overrides to overlay onto the session at startup. [Experimental(Diagnostics.Experimental)] internal sealed class ModelApplyStartupOverlayRequest @@ -8753,13 +8831,14 @@ internal sealed class TasksStartAgentRequest public string SessionId { get; set; } = string.Empty; } -/// Tracked task union returned by task APIs, containing either an agent task or a shell task. +/// Tracked task union returned by task APIs, containing an agent, client, or shell task. /// Polymorphic base type discriminated by type. [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(TaskInfoAgent), "agent")] +[JsonDerivedType(typeof(TaskInfoClient), "client")] [JsonDerivedType(typeof(TaskInfoShell), "shell")] public partial class TaskInfo { @@ -8868,6 +8947,143 @@ public partial class TaskInfoAgent : TaskInfo public required string ToolCallId { get; set; } } +/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. +[Experimental(Diagnostics.Experimental)] +public sealed class TaskClientOwner +{ + /// ISO 8601 timestamp when the bound join disconnected. + [JsonPropertyName("disconnectedAt")] + public DateTimeOffset? DisconnectedAt { get; set; } + + /// Display-only owner name. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Opaque identity of the currently or most recently bound session join. + [JsonPropertyName("joinId")] + public string JoinId { get; set; } = string.Empty; + + /// Class of the task owner. + [JsonPropertyName("kind")] + public TaskClientOwnerKind Kind { get; set; } + + /// Opaque session-scoped participant identity. + [JsonPropertyName("participantId")] + public string ParticipantId { get; set; } = string.Empty; + + /// Whether this task's bound join is currently connected. + [JsonPropertyName("presence")] + public TaskClientOwnerPresence Presence { get; set; } + + /// Display-only owner source. + [JsonPropertyName("source")] + public string? Source { get; set; } +} + +/// Tracked client-owned task metadata. +/// The client variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskInfoClient : TaskInfo +{ + /// + [JsonIgnore] + public override string Type => "client"; + + /// ISO 8601 timestamp when the current active segment started. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } + + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public required long ActiveTimeMs { get; set; } + + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public required bool CanCancel { get; set; } + + /// Human-readable reason for terminal cancellation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { get; set; } + + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public required string ClientTaskId { get; set; } + + /// ISO 8601 timestamp when the task reached a terminal status. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Task description. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// Optional task display name. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Human-readable terminal failure message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Optional owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } + + /// Execution mode, which is always background for client-owned tasks. + [JsonPropertyName("executionMode")] + public required TaskClientExecutionMode ExecutionMode { get; set; } + + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// ISO 8601 timestamp when the connected owner entered idle status. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } + + /// ISO 8601 timestamp of the most recent orphan transition. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("orphanedAt")] + public DateTimeOffset? OrphanedAt { get; set; } + + /// Public attribution and presence for the task owner. + [JsonPropertyName("owner")] + public required TaskClientOwner Owner { get; set; } + + /// ISO 8601 timestamp of the most recent successful reclaim. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reclaimedAt")] + public DateTimeOffset? ReclaimedAt { get; set; } + + /// Opaque successful terminal result supplied by the task owner. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } + + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } + + /// ISO 8601 timestamp when the task started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } + + /// Client task lifecycle status. + [JsonPropertyName("status")] + public required TaskClientStatus Status { get; set; } + + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public required DateTimeOffset UpdatedAt { get; set; } +} + /// Tracked shell task metadata, including ID, command, status, timing, attachment/execution mode, log path, and PID. /// The shell variant of . [Experimental(Diagnostics.Experimental)] @@ -8945,6 +9161,299 @@ internal sealed class SessionTasksListRequest public string SessionId { get; set; } = string.Empty; } +/// Tracked client-owned task metadata. +[Experimental(Diagnostics.Experimental)] +public sealed class TaskClientInfo +{ + /// ISO 8601 timestamp when the current active segment started. + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } + + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public long ActiveTimeMs { get; set; } + + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public bool CanCancel { get; set; } + + /// Human-readable reason for terminal cancellation. + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { get; set; } + + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// ISO 8601 timestamp when the task reached a terminal status. + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Task description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Optional task display name. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Human-readable terminal failure message. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Optional owner-supplied terminal failure code. + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } + + /// Execution mode, which is always background for client-owned tasks. + [JsonPropertyName("executionMode")] + public TaskClientExecutionMode ExecutionMode { get; set; } + + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// ISO 8601 timestamp when the connected owner entered idle status. + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } + + /// ISO 8601 timestamp of the most recent orphan transition. + [JsonPropertyName("orphanedAt")] + public DateTimeOffset? OrphanedAt { get; set; } + + /// Public attribution and presence for the task owner. + [JsonPropertyName("owner")] + public TaskClientOwner Owner { get => field ??= new(); set; } + + /// ISO 8601 timestamp of the most recent successful reclaim. + [JsonPropertyName("reclaimedAt")] + public DateTimeOffset? ReclaimedAt { get; set; } + + /// Opaque successful terminal result supplied by the task owner. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } + + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public long Sequence { get; set; } + + /// ISO 8601 timestamp when the task started. + [JsonPropertyName("startedAt")] + public DateTimeOffset StartedAt { get; set; } + + /// Client task lifecycle status. + [JsonPropertyName("status")] + public TaskClientStatus Status { get; set; } + + /// Task kind. + [JsonPropertyName("type")] + public TaskClientType Type { get; set; } + + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public DateTimeOffset UpdatedAt { get; set; } +} + +/// Result of registering or reclaiming a client-owned task. +[Experimental(Diagnostics.Experimental)] +public sealed class TasksRegisterResult +{ + /// True only when this invocation created a new task. + [JsonPropertyName("created")] + public bool Created { get; set; } + + /// True only when this invocation reclaimed an orphaned task. + [JsonPropertyName("reclaimed")] + public bool Reclaimed { get; set; } + + /// Authoritative registered or reclaimed task. + [JsonPropertyName("task")] + public TaskClientInfo Task { get => field ??= new(); set; } +} + +/// Registers or reclaims a client-owned task. +[Experimental(Diagnostics.Experimental)] +internal sealed class TasksRegisterRequest +{ + /// Whether the owner supports runtime cancellation requests. + [JsonPropertyName("cancellable")] + public bool Cancellable { get; set; } + + /// Owner-scoped idempotency key used for registration and reclaim. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// Human-readable description of the external work. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Optional short display name for the external work. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Expected current sequence for idempotent registration or orphan reclaim. + [JsonPropertyName("expectedSequence")] + public long? ExpectedSequence { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Task kind. + [JsonPropertyName("type")] + public TaskClientType Type { get; set; } +} + +/// Result of publishing a client-owned task update. +[Experimental(Diagnostics.Experimental)] +public sealed class TasksUpdateResult +{ + /// Whether this invocation changed task state. + [JsonPropertyName("applied")] + public bool Applied { get; set; } + + /// Whether this invocation repeated the latest accepted update. + [JsonPropertyName("duplicate")] + public bool Duplicate { get; set; } + + /// Authoritative task after processing the update. + [JsonPropertyName("task")] + public TaskClientInfo Task { get => field ??= new(); set; } +} + +/// Progress or terminal update for a client-owned task. +/// Polymorphic base type discriminated by kind. +[Experimental(Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(TaskClientUpdateProgress), "progress")] +[JsonDerivedType(typeof(TaskClientUpdateCompleted), "completed")] +[JsonDerivedType(typeof(TaskClientUpdateFailed), "failed")] +[JsonDerivedType(typeof(TaskClientUpdateCancelled), "cancelled")] +public partial class TaskClientUpdate +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} + + +/// Publishes nonterminal progress for a running or idle client task. +/// The progress variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskClientUpdateProgress : TaskClientUpdate +{ + /// + [JsonIgnore] + public override string Kind => "progress"; + + /// Optional progress message appended to recent activity when nonempty. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// Optional completion percentage; null clears the current percentage. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } + + /// Optional progress phase; null clears the current phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phase")] + public string? Phase { get; set; } + + /// Optional active status transition. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("status")] + public TaskClientActiveStatus? Status { get; set; } +} + +/// Reports successful terminal completion. +/// The completed variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskClientUpdateCompleted : TaskClientUpdate +{ + /// + [JsonIgnore] + public override string Kind => "completed"; + + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// Optional opaque successful terminal result. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } +} + +/// Reports terminal failure. +/// The failed variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskClientUpdateFailed : TaskClientUpdate +{ + /// + [JsonIgnore] + public override string Kind => "failed"; + + /// Optional owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("code")] + public string? Code { get; set; } + + /// Human-readable terminal failure message. + [JsonPropertyName("error")] + public required string Error { get; set; } + + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +/// Reports terminal cancellation after external work stopped. +/// The cancelled variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskClientUpdateCancelled : TaskClientUpdate +{ + /// + [JsonIgnore] + public override string Kind => "cancelled"; + + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// Optional human-readable cancellation reason. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reason")] + public string? Reason { get; set; } +} + +/// Updates a client-owned task. +[Experimental(Diagnostics.Experimental)] +internal sealed class TasksUpdateRequest +{ + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Owner update sequence to apply. + [JsonPropertyName("sequence")] + public long Sequence { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Progress or terminal update payload. + [JsonPropertyName("update")] + public TaskClientUpdate Update { get => field ??= new(); set; } +} + /// Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop. [Experimental(Diagnostics.Experimental)] public sealed class TasksRefreshResult @@ -8975,13 +9484,16 @@ internal sealed class SessionTasksWaitForPendingRequest public string SessionId { get; set; } = string.Empty; } -/// Polymorphic base type discriminated by type. +/// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. +/// Polymorphic base type discriminated by type. +[Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(TasksGetProgressResultProgressAgent), "agent")] -[JsonDerivedType(typeof(TasksGetProgressResultProgressShell), "shell")] -public partial class TasksGetProgressResultProgress +[JsonDerivedType(typeof(TaskProgressAgent), "agent")] +[JsonDerivedType(typeof(TaskProgressClient), "client")] +[JsonDerivedType(typeof(TaskProgressShell), "shell")] +public partial class TaskProgress { /// The type discriminator. [JsonPropertyName("type")] @@ -9003,8 +9515,9 @@ public sealed class TaskProgressLine } /// Progress snapshot for an agent task, with recent activity lines and optional latest intent. -/// The agent variant of . -public partial class TasksGetProgressResultProgressAgent : TasksGetProgressResultProgress +/// The agent variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskProgressAgent : TaskProgress { /// [JsonIgnore] @@ -9020,9 +9533,51 @@ public partial class TasksGetProgressResultProgressAgent : TasksGetProgressResul public required IList RecentActivity { get; set; } } +/// Generic progress for a client-owned task. +/// The client variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskProgressClient : TaskProgress +{ + /// + [JsonIgnore] + public override string Type => "client"; + + /// Most recent nonempty progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("lastMessage")] + public string? LastMessage { get; set; } + + /// Current completion percentage from zero through one hundred. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } + + /// Current owner-defined progress phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phase")] + public string? Phase { get; set; } + + /// Recent server-timestamped progress messages. + [JsonPropertyName("recentActivity")] + public required IList RecentActivity { get; set; } + + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } + + /// Current client task lifecycle status. + [JsonPropertyName("status")] + public required TaskClientStatus Status { get; set; } + + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public required DateTimeOffset UpdatedAt { get; set; } +} + /// Progress snapshot for a shell task, with recent stdout/stderr output and optional process ID. -/// The shell variant of . -public partial class TasksGetProgressResultProgressShell : TasksGetProgressResultProgress +/// The shell variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskProgressShell : TaskProgress { /// [JsonIgnore] @@ -9044,7 +9599,7 @@ public sealed class TasksGetProgressResult { /// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. [JsonPropertyName("progress")] - public TasksGetProgressResultProgress? Progress { get; set; } + public TaskProgress? Progress { get; set; } } /// Identifier of the background task to fetch progress for. @@ -11113,7 +11668,7 @@ public sealed class OptionsUpdateAdditionalContentExclusionPolicy [Experimental(Diagnostics.Experimental)] public sealed class CapiSessionOptions { - /// Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. + /// Routing preference for sessions whose model is `auto`. On create or cold resume, this establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold resume, the runtime restores the last committed preference. On resident resume, a different value requests a safe switch after resume succeeds and cannot change an in-flight turn. Successful switches are persisted for later cold resume. When no preference is supplied or restored, CAPI default routing is used. [JsonPropertyName("autoTier")] public AutoTier? AutoTier { get; set; } @@ -11356,6 +11911,10 @@ public sealed class SandboxConfig [JsonPropertyName("addCurrentWorkingDirectory")] public bool? AddCurrentWorkingDirectory { get; set; } + /// Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). + [JsonPropertyName("allowBypass")] + public bool? AllowBypass { get; set; } + /// Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). [JsonPropertyName("allowDevToolAccess")] public bool? AllowDevToolAccess { get; set; } @@ -11368,6 +11927,24 @@ public sealed class SandboxConfig [JsonPropertyName("enabled")] public bool Enabled { get; set; } + /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + [JsonInclude] + [JsonPropertyName("managedLspRoutingLocked")] + internal bool? ManagedLspRoutingLocked { get; set; } + + /// Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. + [JsonInclude] + [JsonPropertyName("managedMcpRoutingLocked")] + internal bool? ManagedMcpRoutingLocked { get; set; } + + /// Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxLspServers")] + public bool? SandboxLspServers { get; set; } + + /// Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxMcpServers")] + public bool? SandboxMcpServers { get; set; } + /// User-managed sandbox policy fragment merged into the auto-discovered base policy. [JsonPropertyName("userPolicy")] public SandboxConfigUserPolicy? UserPolicy { get; set; } @@ -18088,6 +18665,40 @@ public sealed class FactoryAbortRequest public string SessionId { get; set; } = string.Empty; } +/// Whether the client authoritatively confirmed its external work stopped. +[Experimental(Diagnostics.Experimental)] +public sealed class ClientTaskCancelResult +{ + /// True only when the owner confirms that external work stopped before responding. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } +} + +/// Runtime-to-owner cancellation request for a client-owned task. +[Experimental(Diagnostics.Experimental)] +public sealed class ClientTaskCancelRequest +{ + /// Opaque identifier shared by coalesced cancellation callers. + [JsonPropertyName("cancellationId")] + public string CancellationId { get; set; } = string.Empty; + + /// Owner-scoped task key included for correlation. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Reason the runtime requests cancellation. + [JsonPropertyName("reason")] + public ClientTaskCancelReason Reason { get; set; } + + /// Session that owns the client task. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Describes a filesystem error. [Experimental(Diagnostics.Experimental)] public sealed class SessionFsError @@ -18941,6 +19552,72 @@ public sealed class GitHubTokenAcquireRequest public string? SessionId { get; set; } } +/// Closed set of public task kinds a connection can negotiate. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Runtime-owned background agent task. + public static TaskKind Agent { get; } = new("agent"); + + /// Runtime-owned shell task. + public static TaskKind Shell { get; } = new("shell"); + + /// Client-owned externally executed task. + public static TaskKind Client { get; } = new("client"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskKind left, TaskKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskKind left, TaskKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskKind other && Equals(other); + + /// + public bool Equals(TaskKind 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 TaskKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskKind)); + } + } +} + + /// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -24179,6 +24856,69 @@ public override void Write(Utf8JsonWriter writer, FactoryLogLineKind value, Json } +/// Whether the requested preference was already effective or was accepted for later transactional activation. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelSwitchAutoTierStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelSwitchAutoTierStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. + public static ModelSwitchAutoTierStatus Unchanged { get; } = new("unchanged"); + + /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. + public static ModelSwitchAutoTierStatus Pending { get; } = new("pending"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ModelSwitchAutoTierStatus other && Equals(other); + + /// + public bool Equals(ModelSwitchAutoTierStatus 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 ModelSwitchAutoTierStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelSwitchAutoTierStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelSwitchAutoTierStatus)); + } + } +} + + /// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -24578,6 +25318,267 @@ public override void Write(Utf8JsonWriter writer, TaskStatus value, JsonSerializ } +/// Client-owned tasks always execute outside the runtime in background mode. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientExecutionMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientExecutionMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the background value. + public static TaskClientExecutionMode Background { get; } = new("background"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientExecutionMode left, TaskClientExecutionMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientExecutionMode left, TaskClientExecutionMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientExecutionMode other && Equals(other); + + /// + public bool Equals(TaskClientExecutionMode 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 TaskClientExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientExecutionMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientExecutionMode)); + } + } +} + + +/// Connection class owning a client task. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientOwnerKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientOwnerKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A discovered extension connection owns the task. + public static TaskClientOwnerKind Extension { get; } = new("extension"); + + /// A generic SDK connection owns the task. + public static TaskClientOwnerKind Sdk { get; } = new("sdk"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerKind left, TaskClientOwnerKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerKind left, TaskClientOwnerKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientOwnerKind other && Equals(other); + + /// + public bool Equals(TaskClientOwnerKind 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 TaskClientOwnerKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientOwnerKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerKind)); + } + } +} + + +/// Presence of the task's bound join. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientOwnerPresence : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientOwnerPresence(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The bound session join is connected. + public static TaskClientOwnerPresence Connected { get; } = new("connected"); + + /// The bound session join is disconnected. + public static TaskClientOwnerPresence Disconnected { get; } = new("disconnected"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientOwnerPresence other && Equals(other); + + /// + public bool Equals(TaskClientOwnerPresence 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 TaskClientOwnerPresence Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientOwnerPresence value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerPresence)); + } + } +} + + +/// Lifecycle status of a client-owned task. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The external owner is actively working. + public static TaskClientStatus Running { get; } = new("running"); + + /// The external owner is connected but waiting. + public static TaskClientStatus Idle { get; } = new("idle"); + + /// The owner reported successful completion. + public static TaskClientStatus Completed { get; } = new("completed"); + + /// The owner reported failure. + public static TaskClientStatus Failed { get; } = new("failed"); + + /// The owner reported or confirmed cancellation. + public static TaskClientStatus Cancelled { get; } = new("cancelled"); + + /// The bound owner join disappeared; external executor state is unknown. + public static TaskClientStatus Orphaned { get; } = new("orphaned"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientStatus left, TaskClientStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientStatus left, TaskClientStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientStatus other && Equals(other); + + /// + public bool Equals(TaskClientStatus 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 TaskClientStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientStatus)); + } + } +} + + /// Whether the shell runs inside a managed PTY session or as an independent background process. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -24641,6 +25642,129 @@ public override void Write(Utf8JsonWriter writer, TaskShellInfoAttachmentMode va } +/// Discriminator for a client-owned task. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the client value. + public static TaskClientType Client { get; } = new("client"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientType left, TaskClientType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientType left, TaskClientType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientType other && Equals(other); + + /// + public bool Equals(TaskClientType 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 TaskClientType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientType)); + } + } +} + + +/// Active status a client owner may publish with a progress update. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientActiveStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientActiveStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The external owner is actively working. + public static TaskClientActiveStatus Running { get; } = new("running"); + + /// The external owner is connected but waiting. + public static TaskClientActiveStatus Idle { get; } = new("idle"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientActiveStatus left, TaskClientActiveStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientActiveStatus left, TaskClientActiveStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientActiveStatus other && Equals(other); + + /// + public bool Equals(TaskClientActiveStatus 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 TaskClientActiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientActiveStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientActiveStatus)); + } + } +} + + /// Consumer allowed to call an MCP tool. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -29479,6 +30603,69 @@ public override void Write(Utf8JsonWriter writer, SessionVisibilityStatus value, } +/// Why the runtime requests client-task cancellation. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ClientTaskCancelReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ClientTaskCancelReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A caller requested task cancellation. + public static ClientTaskCancelReason CancelRequested { get; } = new("cancel_requested"); + + /// The session is shutting down. + public static ClientTaskCancelReason SessionShutdown { get; } = new("session_shutdown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ClientTaskCancelReason left, ClientTaskCancelReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ClientTaskCancelReason left, ClientTaskCancelReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ClientTaskCancelReason other && Equals(other); + + /// + public bool Equals(ClientTaskCancelReason 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 ClientTaskCancelReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ClientTaskCancelReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ClientTaskCancelReason)); + } + } +} + + /// Error classification. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -29887,13 +31074,14 @@ public async Task PingAsync(string? message = null, CancellationToke /// Performs the SDK server connection handshake and validates the optional connection token. Marked internal because this is JSON-RPC transport plumbing invoked automatically by an SDK client's own `connect()` wrapper, not a user-facing method. Stays internal as long as the SDK client owns the handshake; would only become public if the SDK ever exposed the raw schema surface to consumers without a connection wrapper. /// Opt this connection in to GitHub telemetry forwarding for its lifetime. When set, the runtime forwards every internal telemetry event it emits — across all sessions, plus sessionless events — to this connection over the `gitHubTelemetry.event` notification. Regular events are also written to the runtime's normal GitHub/CTS path (dual-write); host-only compatibility events are forward-only and intentionally skip that path. Intended for first-party hosts that re-emit the events into their own telemetry stores. Both unrestricted and restricted events are forwarded, each tagged with a `restricted` discriminator; a backstop drops restricted events when restricted telemetry is disabled — using the process-global gate for ordinary events and an explicit session-scoped decision for host-only events. /// Identity of the integrating host. Optional; omit it to keep the default attribution. + /// Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. /// Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN. /// The to monitor for cancellation requests. The default is . /// Handshake result reporting the server's protocol version and package version on success. [Experimental(Diagnostics.Experimental)] - internal async Task ConnectAsync(bool? enableGitHubTelemetryForwarding = null, ConnectClientInfo? clientInfo = null, string? token = null, CancellationToken cancellationToken = default) + internal async Task ConnectAsync(bool? enableGitHubTelemetryForwarding = null, ConnectClientInfo? clientInfo = null, IList? supportedTaskKinds = null, string? token = null, CancellationToken cancellationToken = default) { - var request = new ConnectRequest { EnableGitHubTelemetryForwarding = enableGitHubTelemetryForwarding, ClientInfo = clientInfo, Token = token }; + var request = new ConnectRequest { EnableGitHubTelemetryForwarding = enableGitHubTelemetryForwarding, ClientInfo = clientInfo, SupportedTaskKinds = supportedTaskKinds, Token = token }; return await CopilotClient.InvokeRpcAsync(_rpc, "connect", [request], cancellationToken); } @@ -30281,12 +31469,13 @@ public async Task UpdateAsync(string name, object config, CancellationToken canc /// Removes an MCP server from user configuration. /// Name of the MCP server to remove. + /// OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. /// The to monitor for cancellation requests. The default is . - public async Task RemoveAsync(string name, CancellationToken cancellationToken = default) + public async Task RemoveAsync(string name, string? authClientIdMetadataUrl = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(name); - var request = new McpConfigRemoveRequest { Name = name }; + var request = new McpConfigRemoveRequest { Name = name, AuthClientIdMetadataUrl = authClientIdMetadataUrl }; await CopilotClient.InvokeRpcAsync(_rpc, "mcp.config.remove", [request], cancellationToken); } @@ -30819,6 +32008,13 @@ public async Task ReadAsync(CancellationToken cancell { return await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.read", [], cancellationToken); } + + /// Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `<cacheHome>/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. + /// The to monitor for cancellation requests. The default is . + public async Task ClearCacheAsync(CancellationToken cancellationToken = default) + { + await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.clearCache", [], cancellationToken); + } } /// Provides server-scoped Runtime APIs. @@ -32303,9 +33499,9 @@ internal ModelApi(CopilotSession session) _session = session; } - /// Gets the currently selected model for the session. + /// Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. /// The to monitor for cancellation requests. The default is . - /// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + /// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. public async Task GetCurrentAsync(CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); @@ -32316,6 +33512,7 @@ public async Task GetCurrentAsync(CancellationToken cancellationTo /// Switches the session to a model and optional reasoning configuration. /// Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. + /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. /// Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. When omitted, no effort override is applied. /// Reasoning summary mode to request for supported model clients. /// Output verbosity level to request for supported models. @@ -32331,15 +33528,28 @@ public async Task GetCurrentAsync(CancellationToken cancellationTo /// Optional settings context and explicit-override flags used to persist a picker selection. /// The to monitor for cancellation requests. The default is . /// The model identifier active on the session after the switch. - public async Task SwitchToAsync(string modelId, string? reasoningEffort = null, ReasoningSummary? reasoningSummary = null, Verbosity? verbosity = null, ModelCapabilitiesOverride? modelCapabilities = null, ContextTier? contextTier = null, ModelChangeSource? source = null, bool? deferIfModelChangeQueued = null, string? compactionDecision = null, bool? runCompactionPreflight = null, string? repoScope = null, string? modelChangeScope = null, bool? requireAvailable = null, ModelPickerPersistenceRequest? pickerPersistence = null, CancellationToken cancellationToken = default) + public async Task SwitchToAsync(string modelId, AutoTier? autoTier = null, string? reasoningEffort = null, ReasoningSummary? reasoningSummary = null, Verbosity? verbosity = null, ModelCapabilitiesOverride? modelCapabilities = null, ContextTier? contextTier = null, ModelChangeSource? source = null, bool? deferIfModelChangeQueued = null, string? compactionDecision = null, bool? runCompactionPreflight = null, string? repoScope = null, string? modelChangeScope = null, bool? requireAvailable = null, ModelPickerPersistenceRequest? pickerPersistence = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(modelId); _session.ThrowIfDisposed(); - var request = new ModelSwitchToRequest { SessionId = _session.SessionId, ModelId = modelId, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ModelCapabilities = modelCapabilities, ContextTier = contextTier, Source = source, DeferIfModelChangeQueued = deferIfModelChangeQueued, CompactionDecision = compactionDecision, RunCompactionPreflight = runCompactionPreflight, RepoScope = repoScope, ModelChangeScope = modelChangeScope, RequireAvailable = requireAvailable, PickerPersistence = pickerPersistence }; + var request = new ModelSwitchToRequest { SessionId = _session.SessionId, ModelId = modelId, AutoTier = autoTier, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ModelCapabilities = modelCapabilities, ContextTier = contextTier, Source = source, DeferIfModelChangeQueued = deferIfModelChangeQueued, CompactionDecision = compactionDecision, RunCompactionPreflight = runCompactionPreflight, RepoScope = repoScope, ModelChangeScope = modelChangeScope, RequireAvailable = requireAvailable, PickerPersistence = pickerPersistence }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.switchTo", [request], cancellationToken); } + /// Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. + /// Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. + /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// The to monitor for cancellation requests. The default is . + /// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + public async Task SwitchAutoTierAsync(AutoTier? autoTier, ModelChangeSource? source = null, CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new ModelSwitchAutoTierRequest { SessionId = _session.SessionId, AutoTier = autoTier, Source = source }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.switchAutoTier", [request], cancellationToken); + } + /// Resolves and applies organization-managed and repository model overlays. /// Model required by device-managed policy, when configured. /// Model required by server-managed policy, when configured. @@ -32961,6 +34171,41 @@ public async Task ListAsync(CancellationToken cancellationToken = defa return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tasks.list", [request], cancellationToken); } + /// Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. + /// Task kind. + /// Owner-scoped idempotency key used for registration and reclaim. + /// Human-readable description of the external work. + /// Whether the owner supports runtime cancellation requests. + /// Optional short display name for the external work. + /// Expected current sequence for idempotent registration or orphan reclaim. + /// The to monitor for cancellation requests. The default is . + /// Result of registering or reclaiming a client-owned task. + public async Task RegisterAsync(TaskClientType type, string clientTaskId, string description, bool cancellable, string? displayName = null, long? expectedSequence = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(clientTaskId); + ArgumentNullException.ThrowIfNull(description); + _session.ThrowIfDisposed(); + + var request = new TasksRegisterRequest { SessionId = _session.SessionId, Type = type, ClientTaskId = clientTaskId, Description = description, Cancellable = cancellable, DisplayName = displayName, ExpectedSequence = expectedSequence }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tasks.register", [request], cancellationToken); + } + + /// Publishes generic progress or a terminal outcome for a client-owned task. + /// Canonical runtime-generated task identifier. + /// Owner update sequence to apply. + /// Progress or terminal update payload. + /// The to monitor for cancellation requests. The default is . + /// Result of publishing a client-owned task update. + public async Task UpdateAsync(string id, long sequence, TaskClientUpdate update, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(id); + ArgumentNullException.ThrowIfNull(update); + _session.ThrowIfDisposed(); + + var request = new TasksUpdateRequest { SessionId = _session.SessionId, Id = id, Sequence = sequence, Update = update }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tasks.update", [request], cancellationToken); + } + /// Refreshes metadata for any detached background shells the runtime knows about. /// The to monitor for cancellation requests. The default is . /// Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop. @@ -35664,6 +36909,17 @@ public interface IFactoryHandler Task AbortAsync(FactoryAbortRequest request, CancellationToken cancellationToken = default); } +/// Handles `tasks` client session API methods. +[Experimental(Diagnostics.Experimental)] +public interface ITasksHandler +{ + /// Asks the client currently bound to a client-owned session task to confirm that its external work stopped. + /// Runtime-to-owner cancellation request for a client-owned task. + /// The to monitor for cancellation requests. The default is . + /// Whether the client authoritatively confirmed its external work stopped. + Task CancelAsync(ClientTaskCancelRequest request, CancellationToken cancellationToken = default); +} + /// Handles `sessionFs` client session API methods. [Experimental(Diagnostics.Experimental)] public interface ISessionFsHandler @@ -35764,6 +37020,9 @@ public sealed class ClientSessionApiHandlers /// Optional handler for Factory client session API methods. public IFactoryHandler? Factory { get; set; } + /// Optional handler for Tasks client session API methods. + public ITasksHandler? Tasks { get; set; } + /// Optional handler for SessionFs client session API methods. public ISessionFsHandler? SessionFs { get; set; } @@ -35799,6 +37058,12 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, Func>)(async (request, cancellationToken) => + { + var handler = getHandlers(request.SessionId).Tasks; + if (handler is null) throw new InvalidOperationException($"No tasks handler registered for session: {request.SessionId}"); + return await handler.CancelAsync(request, cancellationToken); + }), singleObjectParam: true); rpc.SetLocalRpcMethod("sessionFs.readFile", (Func>)(async (request, cancellationToken) => { var handler = getHandlers(request.SessionId).SessionFs; @@ -36092,6 +37357,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchRequestedEvent), TypeInfoPropertyName = "SessionEventsAutoModeSwitchRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchResponse), TypeInfoPropertyName = "SessionEventsAutoModeSwitchResponse")] [JsonSerializable(typeof(GitHub.Copilot.AutoTier), TypeInfoPropertyName = "SessionEventsAutoTier")] +[JsonSerializable(typeof(GitHub.Copilot.AutoTierSwitchFailureReason), TypeInfoPropertyName = "SessionEventsAutoTierSwitchFailureReason")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedOperation), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedOperation")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedStatus), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedStatus")] [JsonSerializable(typeof(GitHub.Copilot.BinaryAssetReference), TypeInfoPropertyName = "SessionEventsBinaryAssetReference")] @@ -36469,6 +37735,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(CatalogNegotiatedContract))] [JsonSerializable(typeof(CatalogSearchRequest))] [JsonSerializable(typeof(CatalogSearchResult))] +[JsonSerializable(typeof(ClientTaskCancelRequest))] +[JsonSerializable(typeof(ClientTaskCancelResult))] [JsonSerializable(typeof(CommandList))] [JsonSerializable(typeof(CommandsFinalizeInvocationEffectRequest))] [JsonSerializable(typeof(CommandsFinalizeInvocationEffectRequestEffect))] @@ -36775,6 +38043,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModelPolicy))] [JsonSerializable(typeof(ModelSetReasoningEffortRequest))] [JsonSerializable(typeof(ModelSetReasoningEffortResult))] +[JsonSerializable(typeof(ModelSwitchAutoTierRequest))] +[JsonSerializable(typeof(ModelSwitchAutoTierResult))] [JsonSerializable(typeof(ModelSwitchConfirmation))] [JsonSerializable(typeof(ModelSwitchToRequest))] [JsonSerializable(typeof(ModelSwitchToResult))] @@ -37195,27 +38465,34 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SlashCommandSelectSubcommandOption))] [JsonSerializable(typeof(SlashCommandTimelineEntry))] [JsonSerializable(typeof(SubagentSettingsEntry))] +[JsonSerializable(typeof(TaskClientInfo))] +[JsonSerializable(typeof(TaskClientOwner))] +[JsonSerializable(typeof(TaskClientUpdate))] [JsonSerializable(typeof(TaskCompleteData))] [JsonSerializable(typeof(TaskCompletionDecision))] [JsonSerializable(typeof(TaskInfo))] [JsonSerializable(typeof(TaskList))] +[JsonSerializable(typeof(TaskProgress))] [JsonSerializable(typeof(TaskProgressLine))] [JsonSerializable(typeof(TasksCancelRequest))] [JsonSerializable(typeof(TasksCancelResult))] [JsonSerializable(typeof(TasksGetCurrentPromotableResult))] [JsonSerializable(typeof(TasksGetProgressRequest))] [JsonSerializable(typeof(TasksGetProgressResult))] -[JsonSerializable(typeof(TasksGetProgressResultProgress))] [JsonSerializable(typeof(TasksPromoteCurrentToBackgroundResult))] [JsonSerializable(typeof(TasksPromoteToBackgroundRequest))] [JsonSerializable(typeof(TasksPromoteToBackgroundResult))] [JsonSerializable(typeof(TasksRefreshResult))] +[JsonSerializable(typeof(TasksRegisterRequest))] +[JsonSerializable(typeof(TasksRegisterResult))] [JsonSerializable(typeof(TasksRemoveRequest))] [JsonSerializable(typeof(TasksRemoveResult))] [JsonSerializable(typeof(TasksSendMessageRequest))] [JsonSerializable(typeof(TasksSendMessageResult))] [JsonSerializable(typeof(TasksStartAgentRequest))] [JsonSerializable(typeof(TasksStartAgentResult))] +[JsonSerializable(typeof(TasksUpdateRequest))] +[JsonSerializable(typeof(TasksUpdateResult))] [JsonSerializable(typeof(TasksWaitForPendingResult))] [JsonSerializable(typeof(TelemetrySetFeatureOverridesRequest))] [JsonSerializable(typeof(Tool))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 147e08e458..e7890dee92 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -84,6 +84,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionLimitsExhaustedCompletedEvent), "session_limits_exhausted.completed")] [JsonDerivedType(typeof(SessionLimitsExhaustedRequestedEvent), "session_limits_exhausted.requested")] [JsonDerivedType(typeof(SessionAutoModeResolvedEvent), "session.auto_mode_resolved")] +[JsonDerivedType(typeof(SessionAutoTierSwitchFailedEvent), "session.auto_tier_switch_failed")] [JsonDerivedType(typeof(SessionAutopilotObjectiveChangedEvent), "session.autopilot_objective_changed")] [JsonDerivedType(typeof(SessionBackgroundTasksChangedEvent), "session.background_tasks_changed")] [JsonDerivedType(typeof(SessionBinaryAssetEvent), "session.binary_asset")] @@ -112,6 +113,8 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionInfoEvent), "session.info")] [JsonDerivedType(typeof(SessionManagedSettingsEnforcedEvent), "session.managed_settings_enforced")] [JsonDerivedType(typeof(SessionManagedSettingsResolvedEvent), "session.managed_settings_resolved")] +[JsonDerivedType(typeof(SessionMcpServerNeedsReconnectEvent), "session.mcp_server_needs_reconnect")] +[JsonDerivedType(typeof(SessionMcpServerRemovedEvent), "session.mcp_server_removed")] [JsonDerivedType(typeof(SessionMcpServerStatusChangedEvent), "session.mcp_server_status_changed")] [JsonDerivedType(typeof(SessionMcpServersLoadedEvent), "session.mcp_servers_loaded")] [JsonDerivedType(typeof(SessionModeChangedEvent), "session.mode_changed")] @@ -368,6 +371,19 @@ public sealed partial class SessionModelChangeEvent : SessionEvent public required SessionModelChangeData Data { get; set; } } +/// A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. +/// Represents the session.auto_tier_switch_failed event. +public sealed partial class SessionAutoTierSwitchFailedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.auto_tier_switch_failed"; + + /// The session.auto_tier_switch_failed event payload. + [JsonPropertyName("data")] + public required SessionAutoTierSwitchFailedData Data { get; set; } +} + /// Agent mode change details including previous and new modes. /// Represents the session.mode_changed event. public sealed partial class SessionModeChangedEvent : SessionEvent @@ -1790,6 +1806,32 @@ public sealed partial class SessionMcpServerStatusChangedEvent : SessionEvent public required SessionMcpServerStatusChangedData Data { get; set; } } +/// Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. +/// Represents the session.mcp_server_removed event. +public sealed partial class SessionMcpServerRemovedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.mcp_server_removed"; + + /// The session.mcp_server_removed event payload. + [JsonPropertyName("data")] + public required SessionMcpServerRemovedData Data { get; set; } +} + +/// Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. +/// Represents the session.mcp_server_needs_reconnect event. +public sealed partial class SessionMcpServerNeedsReconnectEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.mcp_server_needs_reconnect"; + + /// The session.mcp_server_needs_reconnect event payload. + [JsonPropertyName("data")] + public required SessionMcpServerNeedsReconnectData Data { get; set; } +} + /// Payload identifying the MCP server associated with a list change. /// Represents the mcp.tools.list_changed event. public sealed partial class McpToolsListChangedEvent : SessionEvent @@ -2323,6 +2365,11 @@ public sealed partial class SessionWarningData /// Model change details including previous and new model identifiers. public sealed partial class SessionModelChangeData { + /// Committed Auto preference after the model configuration change, when applicable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cause")] @@ -2337,6 +2384,11 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("newModel")] public required string NewModel { get; set; } + /// Previously committed Auto preference, when one was explicitly selected. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("previousAutoTier")] + public AutoTier? PreviousAutoTier { get; set; } + /// Model that was previously selected, if any. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("previousModel")] @@ -2378,6 +2430,23 @@ public sealed partial class SessionModelChangeData public Verbosity? Verbosity { get; set; } } +/// A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. +public sealed partial class SessionAutoTierSwitchFailedData +{ + /// Auto preference that remains effective after the failed request. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("effectiveAutoTier")] + public AutoTier? EffectiveAutoTier { get; set; } + + /// Low-cardinality failure outcome reported by Auto resolution. + [JsonPropertyName("reason")] + public required AutoTierSwitchFailureReason Reason { get; set; } + + /// Auto preference that failed to activate, or null when returning to provider-default routing failed. + [JsonPropertyName("requestedAutoTier")] + public AutoTier? RequestedAutoTier { get; set; } +} + /// Agent mode change details including previous and new modes. public sealed partial class SessionModeChangedData { @@ -5855,6 +5924,22 @@ public sealed partial class SessionMcpServerStatusChangedData public required McpServerStatus Status { get; set; } } +/// Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. +public sealed partial class SessionMcpServerRemovedData +{ + /// Name of the MCP server that was removed from the graph. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + +/// Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. +public sealed partial class SessionMcpServerNeedsReconnectData +{ + /// Name of the MCP server that needs to reconnect. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + /// Payload identifying the MCP server associated with a list change. public sealed partial class McpToolsListChangedData { @@ -11233,6 +11318,73 @@ public override void Write(Utf8JsonWriter writer, ModelChangeSource value, JsonS } } +/// Terminal reason an Auto preference activation failed. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AutoTierSwitchFailureReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AutoTierSwitchFailureReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The candidate model was rejected by model policy. + public static AutoTierSwitchFailureReason PolicyRejected { get; } = new("policy_rejected"); + + /// The Auto routing request failed or returned an unusable response. + public static AutoTierSwitchFailureReason RequestFailed { get; } = new("request_failed"); + + /// The runtime could not prepare the Auto routing request. + public static AutoTierSwitchFailureReason SetupFailed { get; } = new("setup_failed"); + + /// The provider does not support Auto routing. + public static AutoTierSwitchFailureReason Unsupported { get; } = new("unsupported"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutoTierSwitchFailureReason left, AutoTierSwitchFailureReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutoTierSwitchFailureReason left, AutoTierSwitchFailureReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AutoTierSwitchFailureReason other && Equals(other); + + /// + public bool Equals(AutoTierSwitchFailureReason 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 AutoTierSwitchFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AutoTierSwitchFailureReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutoTierSwitchFailureReason)); + } + } +} + /// Permission mode for the session. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -16307,6 +16459,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SandboxDecisionEvent))] [JsonSerializable(typeof(SessionAutoModeResolvedData))] [JsonSerializable(typeof(SessionAutoModeResolvedEvent))] +[JsonSerializable(typeof(SessionAutoTierSwitchFailedData))] +[JsonSerializable(typeof(SessionAutoTierSwitchFailedEvent))] [JsonSerializable(typeof(SessionAutopilotObjectiveChangedData))] [JsonSerializable(typeof(SessionAutopilotObjectiveChangedEvent))] [JsonSerializable(typeof(SessionBackgroundTasksChangedData))] @@ -16370,6 +16524,10 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionManagedSettingsEnforcedEvent))] [JsonSerializable(typeof(SessionManagedSettingsResolvedData))] [JsonSerializable(typeof(SessionManagedSettingsResolvedEvent))] +[JsonSerializable(typeof(SessionMcpServerNeedsReconnectData))] +[JsonSerializable(typeof(SessionMcpServerNeedsReconnectEvent))] +[JsonSerializable(typeof(SessionMcpServerRemovedData))] +[JsonSerializable(typeof(SessionMcpServerRemovedEvent))] [JsonSerializable(typeof(SessionMcpServerStatusChangedData))] [JsonSerializable(typeof(SessionMcpServerStatusChangedEvent))] [JsonSerializable(typeof(SessionMcpServersLoadedData))] diff --git a/dotnet/test/E2E/McpOAuthE2ETests.cs b/dotnet/test/E2E/McpOAuthE2ETests.cs index 1085aba040..e947ea7640 100644 --- a/dotnet/test/E2E/McpOAuthE2ETests.cs +++ b/dotnet/test/E2E/McpOAuthE2ETests.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Net.Http; using System.Text.Json; +using System.Threading.Channels; using Xunit; using Xunit.Abstractions; @@ -48,6 +49,7 @@ public async Task Should_Satisfy_MCP_OAuth_Using_Host_Provided_Token() } }); + await session.Rpc.Mcp.ReloadAsync(); await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); var tools = await session.Rpc.Mcp.ListToolsAsync(serverName); Assert.Contains(tools.Tools, tool => tool.Name == "whoami"); @@ -75,14 +77,14 @@ public async Task Should_Resolve_Pending_MCP_OAuth_Request_With_Direct_Rpc() { await using var oauthServer = await OAuthMcpServer.StartAsync(ExpectedToken); var serverName = "oauth-direct-rpc-mcp"; - var authRequest = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var authRequests = Channel.CreateUnbounded(); var releaseHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await using var session = await CreateSessionAsync(new SessionConfig { OnMcpAuthRequest = request => { - authRequest.TrySetResult(request); + authRequests.Writer.TryWrite(request); return releaseHandler.Task; }, McpServers = new Dictionary @@ -95,8 +97,27 @@ public async Task Should_Resolve_Pending_MCP_OAuth_Request_With_Direct_Rpc() }, }); + var reload = session.Rpc.Mcp.ReloadAsync(); var connected = WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); - var request = await authRequest.Task.WaitAsync(TimeSpan.FromSeconds(30)); + var request = await authRequests.Reader.ReadAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(30)); + + while (true) + { + var handled = await session.Rpc.Mcp.Oauth.HandlePendingRequestAsync( + request.RequestId, + new McpOauthPendingRequestResponseToken + { + AccessToken = ExpectedToken, + TokenType = "Bearer", + ExpiresIn = 3600, + }); + if (handled.Success) + { + break; + } + request = await authRequests.Reader.ReadAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(30)); + } + Assert.NotEmpty(request.RequestId); Assert.Equal(serverName, request.ServerName); Assert.Equal($"{oauthServer.Url}/mcp", request.ServerUrl); @@ -104,21 +125,11 @@ public async Task Should_Resolve_Pending_MCP_OAuth_Request_With_Direct_Rpc() Assert.NotNull(request.WwwAuthenticateParams); Assert.Equal("mcp.read", request.WwwAuthenticateParams!.Scope); - var handled = await session.Rpc.Mcp.Oauth.HandlePendingRequestAsync( - request.RequestId, - new McpOauthPendingRequestResponseToken - { - AccessToken = ExpectedToken, - TokenType = "Bearer", - ExpiresIn = 3600, - }); - Assert.True(handled.Success); - + releaseHandler.SetResult(McpAuthResult.FromToken(new McpAuthToken { AccessToken = ExpectedToken })); + await reload; await connected; var tools = await session.Rpc.Mcp.ListToolsAsync(serverName); Assert.Contains(tools.Tools, tool => tool.Name == "whoami"); - - releaseHandler.SetResult(McpAuthResult.FromToken(new McpAuthToken { AccessToken = ExpectedToken })); } [Fact] @@ -178,14 +189,16 @@ public async Task Should_Request_Replacement_Tokens_Across_MCP_OAuth_Lifecycle() } }); + await session.Rpc.Mcp.ReloadAsync(); await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); + refreshCount = 0; await CallWhoamiAsync(session, serverName, "refresh"); await CallWhoamiAsync(session, serverName, "upscope"); await CallWhoamiAsync(session, serverName, "reauth"); + observedReasons.RemoveAll(reason => reason == McpOauthRequestReason.Initial); Assert.Equal( [ - McpOauthRequestReason.Initial, McpOauthRequestReason.Refresh, McpOauthRequestReason.Upscope, McpOauthRequestReason.Refresh, @@ -223,6 +236,7 @@ public async Task Should_Cancel_Pending_MCP_OAuth_Request() } }); + await session.Rpc.Mcp.ReloadAsync(); await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.NeedsAuth); // The MCP connection is kicked off by session.create, but the SDK only registers its diff --git a/dotnet/test/Unit/GitHubTelemetryTests.cs b/dotnet/test/Unit/GitHubTelemetryTests.cs index 6d76af5742..d3e7672da0 100644 --- a/dotnet/test/Unit/GitHubTelemetryTests.cs +++ b/dotnet/test/Unit/GitHubTelemetryTests.cs @@ -138,6 +138,9 @@ public async Task Connect_Does_Not_Opt_In_Without_Handler() Assert.True( !present || flag.ValueKind == JsonValueKind.Null, "connect request should omit enableGitHubTelemetryForwarding (or send null) when no handler is registered"); + Assert.Equal( + ["agent", "client", "shell"], + connectParams.GetProperty("supportedTaskKinds").EnumerateArray().Select(kind => kind.GetString())); } [Fact] diff --git a/go/client.go b/go/client.go index ba77402ddb..45de4f6e92 100644 --- a/go/client.go +++ b/go/client.go @@ -1968,7 +1968,14 @@ func (c *Client) verifyProtocolVersion(ctx context.Context) error { t := c.effectiveConnectionToken tokenPtr = &t } - connectReq := &connectHandshakeRequest{Token: tokenPtr} + connectReq := &connectHandshakeRequest{ + Token: tokenPtr, + SupportedTaskKinds: []rpc.TaskKind{ + rpc.TaskKindAgent, + rpc.TaskKindClient, + rpc.TaskKindShell, + }, + } // Opt in to GitHub telemetry forwarding at the connection level when a handler is // registered (mirrors the runtime, which reads this flag on the `connect` handshake // so the first session's un-replayable `session.start` event is forwarded). Also @@ -2019,6 +2026,7 @@ type connectHandshakeRequest struct { Token *string `json:"token,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` ClientInfo *rpc.ConnectClientInfo `json:"clientInfo,omitempty"` + SupportedTaskKinds []rpc.TaskKind `json:"supportedTaskKinds,omitempty"` } // stderrBufferSize is the maximum number of bytes kept from the CLI process's diff --git a/go/client_test.go b/go/client_test.go index ee0e078fb1..b573bd3dca 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -2316,6 +2316,21 @@ func TestClient_ResumeSession_AllowsMissingPermissionHandler(t *testing.T) { }) } +func TestModelInfoPreservesProviderMetadata(t *testing.T) { + var model ModelInfo + if err := json.Unmarshal([]byte(`{ + "id":"provider/model", + "name":"Provider Model", + "capabilities":{"supports":{},"limits":{}}, + "metadata":{"provider":"acme","contextWindow":200000} + }`), &model); err != nil { + t.Fatalf("unmarshal model info: %v", err) + } + if model.Metadata["provider"] != "acme" || model.Metadata["contextWindow"] != float64(200000) { + t.Fatalf("metadata = %#v", model.Metadata) + } +} + func TestListModelsWithCustomHandler(t *testing.T) { customModels := []ModelInfo{ { @@ -3916,7 +3931,9 @@ func TestClient_ForwardsGitHubTelemetryForwardingOnConnect(t *testing.T) { if err := client.verifyProtocolVersion(t.Context()); err != nil { t.Fatalf("verifyProtocolVersion failed: %v", err) } - assertForwardingFlagTrue(t, <-connectParams) + params := <-connectParams + assertForwardingFlagTrue(t, params) + assertSupportedTaskKinds(t, params) } func TestClient_OmitsGitHubTelemetryForwardingOnConnectWhenNoHandler(t *testing.T) { @@ -3942,6 +3959,20 @@ func TestClient_OmitsGitHubTelemetryForwardingOnConnectWhenNoHandler(t *testing. assertForwardingFlagAbsent(t, <-connectParams) } +func assertSupportedTaskKinds(t *testing.T, params json.RawMessage) { + t.Helper() + var decoded struct { + SupportedTaskKinds []rpc.TaskKind `json:"supportedTaskKinds"` + } + if err := json.Unmarshal(params, &decoded); err != nil { + t.Fatalf("unmarshal connect params: %v", err) + } + expected := []rpc.TaskKind{rpc.TaskKindAgent, rpc.TaskKindClient, rpc.TaskKindShell} + if !reflect.DeepEqual(decoded.SupportedTaskKinds, expected) { + t.Fatalf("supportedTaskKinds = %v, want %v", decoded.SupportedTaskKinds, expected) + } +} + func TestGitHubTelemetryNotificationRoutesToCallback(t *testing.T) { // The runtime forwards telemetry via a JSON-RPC *notification* (no id). // Drive a real Content-Length-framed notification through the transport and diff --git a/go/internal/e2e/mcp_oauth_e2e_test.go b/go/internal/e2e/mcp_oauth_e2e_test.go index 95de73eddd..27e6092e11 100644 --- a/go/internal/e2e/mcp_oauth_e2e_test.go +++ b/go/internal/e2e/mcp_oauth_e2e_test.go @@ -163,16 +163,21 @@ func TestMCPOAuthE2E(t *testing.T) { } t.Cleanup(func() { session.Disconnect() }) + if _, err := session.RPC.MCP.Reload(t.Context()); err != nil { + t.Fatalf("Failed to reload MCP servers: %v", err) + } waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) callWhoami(t, session, serverName, "refresh") callWhoami(t, session, serverName, "upscope") callWhoami(t, session, serverName, "reauth") mu.Lock() + observedReasons = slices.DeleteFunc(observedReasons, func(reason copilot.MCPOauthRequestReason) bool { + return reason == copilot.MCPOauthRequestReasonInitial + }) reasons := append([]copilot.MCPOauthRequestReason(nil), observedReasons...) mu.Unlock() expectedReasons := []copilot.MCPOauthRequestReason{ - copilot.MCPOauthRequestReasonInitial, copilot.MCPOauthRequestReasonRefresh, copilot.MCPOauthRequestReasonUpscope, copilot.MCPOauthRequestReasonRefresh, diff --git a/go/rpc/generated_rpc_api_shape_test.go b/go/rpc/generated_rpc_api_shape_test.go index 05774f4847..dcfe0feb8d 100644 --- a/go/rpc/generated_rpc_api_shape_test.go +++ b/go/rpc/generated_rpc_api_shape_test.go @@ -40,6 +40,9 @@ func TestGeneratedRPCAPIShape(t *testing.T) { assertStructFieldType(t, file, fileSet, "MCPConfigUpdateRequest", "Config", "MCPSerializableServerConfig") assertStructFieldType(t, file, fileSet, "MCPServerConfigHTTP", "FilterMapping", "FilterMapping") assertStructFieldType(t, file, fileSet, "MCPServerConfigStdio", "FilterMapping", "FilterMapping") + assertStructFieldType(t, file, fileSet, "ModelSwitchToRequest", "AutoTier", "**AutoTier") + assertStructFieldType(t, file, fileSet, "TaskClientUpdateProgress", "Percentage", "**float64") + assertStructFieldType(t, file, fileSet, "TaskClientUpdateProgress", "Phase", "**string") assertInterfaceType(t, file, "UIElicitationFieldValue") assertTypeExpr(t, fileSet, findTypeSpec(t, file, "UIElicitationStringArrayValue").Type, "[]string") diff --git a/go/rpc/generated_rpc_union_test.go b/go/rpc/generated_rpc_union_test.go index 92bcb4c077..64d723f21a 100644 --- a/go/rpc/generated_rpc_union_test.go +++ b/go/rpc/generated_rpc_union_test.go @@ -14,6 +14,7 @@ func TestExternalToolResultJSONUnion(t *testing.T) { if err != nil { t.Fatalf("marshal string result: %v", err) } + if string(raw) != `"tool result"` { t.Fatalf("marshal string result = %s", raw) } @@ -46,6 +47,29 @@ func TestExternalToolResultJSONUnion(t *testing.T) { } } +func TestOptionalNullableFieldsPreserveOmittedAndNull(t *testing.T) { + unset, err := json.Marshal(TaskClientUpdateProgress{}) + if err != nil { + t.Fatalf("marshal unset progress: %v", err) + } + if string(unset) != `{"kind":"progress"}` { + t.Fatalf("marshal unset progress = %s", unset) + } + + var clearedPercentage *float64 + var clearedPhase *string + cleared, err := json.Marshal(TaskClientUpdateProgress{ + Percentage: &clearedPercentage, + Phase: &clearedPhase, + }) + if err != nil { + t.Fatalf("marshal cleared progress: %v", err) + } + if string(cleared) != `{"kind":"progress","percentage":null,"phase":null}` { + t.Fatalf("marshal cleared progress = %s", cleared) + } +} + func TestFilterMappingJSONUnion(t *testing.T) { var mapping FilterMapping = FilterMappingEnumMap{"secret": ContentFilterModeHiddenCharacters} raw, err := json.Marshal(mapping) diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 5b4cdab32d..4eb60ffd3c 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -1366,9 +1366,12 @@ type CanvasSessionContext struct { // Experimental: CapiSessionOptions is part of an experimental API and may change or be // removed. type CapiSessionOptions struct { - // Routing preference used when the session model is `auto`. The runtime persists the - // preference across cold resume. When omitted, the default routing behavior is used. - // Resuming an already-resident session cannot change its preference. + // Routing preference for sessions whose model is `auto`. On create or cold resume, this + // establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold + // resume, the runtime restores the last committed preference. On resident resume, a + // different value requests a safe switch after resume succeeds and cannot change an + // in-flight turn. Successful switches are persisted for later cold resume. When no + // preference is supplied or restored, CAPI default routing is used. AutoTier *AutoTier `json:"autoTier,omitempty"` // Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when // the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses @@ -1850,6 +1853,30 @@ func (CatalogUnsupportedKindError) Kind() CatalogSearchResultKind { return CatalogSearchResultKindUnsupportedKind } +// Runtime-to-owner cancellation request for a client-owned task. +// Experimental: ClientTaskCancelRequest is part of an experimental API and may change or be +// removed. +type ClientTaskCancelRequest struct { + // Opaque identifier shared by coalesced cancellation callers + CancellationID string `json:"cancellationId"` + // Owner-scoped task key included for correlation + ClientTaskID string `json:"clientTaskId"` + // Canonical runtime-generated task identifier + ID string `json:"id"` + // Reason the runtime requests cancellation + Reason ClientTaskCancelReason `json:"reason"` + // Session that owns the client task + SessionID string `json:"sessionId"` +} + +// Whether the client authoritatively confirmed its external work stopped. +// Experimental: ClientTaskCancelResult is part of an experimental API and may change or be +// removed. +type ClientTaskCancelResult struct { + // True only when the owner confirms that external work stopped before responding + Cancelled bool `json:"cancelled"` +} + // Slash commands available in the session, after applying any include/exclude filters. // Experimental: CommandList is part of an experimental API and may change or be removed. type CommandList struct { @@ -2073,6 +2100,9 @@ type ConnectRequest struct { // using the process-global gate for ordinary events and an explicit session-scoped decision // for host-only events. EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` + // Task kinds this connection can decode when observing session tasks. Omit to retain agent + // and shell compatibility. + SupportedTaskKinds []TaskKind `json:"supportedTaskKinds,omitzero"` // Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN Token *string `json:"token,omitempty"` } @@ -2085,6 +2115,8 @@ type ConnectResult struct { Ok bool `json:"ok"` // Server protocol version number ProtocolVersion int64 `json:"protocolVersion"` + // Task kinds the server may return to this connection. + TaskKinds []TaskKind `json:"taskKinds,omitzero"` // Server package version Version string `json:"version"` } @@ -2341,15 +2373,24 @@ type CopilotUserResponseQuotaSnapshotsPremiumInteractions struct { Unlimited *bool `json:"unlimited,omitempty"` } -// The currently selected model, reasoning effort, and context tier for the session. The -// context tier reflects `Session.getContextTier()`, restored from the session journal on -// resume. +// The session's authoritative model snapshot. Auto preference fields are configuration for +// the virtual `auto` model and do not change the selected model identifier. The context +// tier reflects `Session.getContextTier()`, restored from the session journal on resume. // Experimental: CurrentModel is part of an experimental API and may change or be removed. type CurrentModel struct { + // Auto preference currently claimed by an in-progress activation. Null means the activation + // is returning to provider-default routing. + ActivatingAutoTier *AutoTier `json:"activatingAutoTier,omitempty"` + // Auto preference currently committed for the session. This can remain available while + // another model is selected so a later switch to `auto` can reuse it. + AutoTier *AutoTier `json:"autoTier,omitempty"` // Context tier for models that support multiple context-window sizes. ContextTier *ContextTier `json:"contextTier,omitempty"` // Currently active model identifier ModelID *string `json:"modelId,omitempty"` + // Latest unclaimed Auto preference waiting for a future user turn. Null means the pending + // request is returning to provider-default routing. + PendingAutoTier *AutoTier `json:"pendingAutoTier,omitempty"` // Reasoning effort level currently applied to the active model, when one is set. Reads // `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the // two values are reported as a snapshot. @@ -4772,6 +4813,11 @@ type LspInitializeRequest struct { WorkingDirectory *string `json:"workingDirectory,omitempty"` } +// Experimental: ManagedSettingsClearCacheResult is part of an experimental API and may +// change or be removed. +type ManagedSettingsClearCacheResult struct { +} + // Validated device-managed settings discovered before a session exists. // Experimental: ManagedSettingsReadResult is part of an experimental API and may change or // be removed. @@ -5122,6 +5168,8 @@ type MCPConfigReloadResult struct { // Experimental: MCPConfigRemoveRequest is part of an experimental API and may change or be // removed. type MCPConfigRemoveRequest struct { + // OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. + AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` // Name of the MCP server to remove Name string `json:"name"` } @@ -6933,6 +6981,10 @@ type Model struct { // a recommended alternative. Present only when the service published at least one notice. // Hosts should surface these without implying anything is wrong with the model. InfoMessages []ModelMessage `json:"infoMessages,omitzero"` + // Provider-supplied model metadata. Keys and JSON-compatible values are preserved + // unchanged. This is factual metadata published by the model provider; it carries no picker + // or UX semantics. + Metadata map[string]any `json:"metadata,omitzero"` // Model capability category for grouping in the model picker ModelPickerCategory *ModelPickerCategory `json:"modelPickerCategory,omitempty"` // Relative cost tier for token-based billing users @@ -7261,6 +7313,38 @@ type ModelsListRequest struct { SelectionID *string `json:"selectionId,omitempty"` } +// An Auto preference request for the session. This updates Auto configuration only; it does +// not change the selected model to `auto`. +// Experimental: ModelSwitchAutoTierRequest is part of an experimental API and may change or +// be removed. +type ModelSwitchAutoTierRequest struct { + // Auto preference to activate when a future user turn using the `auto` model safely mints a + // replacement model and token pair. Pass null to return to provider-default Auto routing. + AutoTier *AutoTier `json:"autoTier"` + // Origin to record on the effective `session.model_change` event. Defaults to `sdk` when + // omitted. + Source *ModelChangeSource `json:"source,omitempty"` +} + +// Immediate acknowledgement and Auto preference snapshot after a switch request. This +// result never implies that a pending preference committed. +// Experimental: ModelSwitchAutoTierResult is part of an experimental API and may change or +// be removed. +type ModelSwitchAutoTierResult struct { + // Auto preference currently claimed by an in-progress activation. Null means the activation + // is returning to provider-default routing. + ActivatingAutoTier *AutoTier `json:"activatingAutoTier,omitempty"` + // Auto preference currently committed for the session. + EffectiveAutoTier *AutoTier `json:"effectiveAutoTier,omitempty"` + // Latest unclaimed Auto preference waiting for a future user turn. + PendingAutoTier *AutoTier `json:"pendingAutoTier,omitempty"` + // Immediate request status. `pending` means accepted but not committed. + Status ModelSwitchAutoTierStatus `json:"status"` + // Earlier unclaimed preference replaced by this request. This can be present with either + // status, including when selecting the effective preference cancels pending work. + SupersededAutoTier *AutoTier `json:"supersededAutoTier,omitempty"` +} + // Experimental: ModelSwitchConfirmation is part of an experimental API and may change or be // removed. type ModelSwitchConfirmation struct { @@ -7277,6 +7361,10 @@ type ModelSwitchConfirmation struct { // Experimental: ModelSwitchToRequest is part of an experimental API and may change or be // removed. type ModelSwitchToRequest struct { + // Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to + // return to provider-default Auto routing. This field is rejected when `modelId` is not + // `auto`. + AutoTier **AutoTier `json:"autoTier,omitempty"` // Explicit response to a model-switch compaction preflight. Omit to request a confirmation // projection when compaction is necessary. CompactionDecision *string `json:"compactionDecision,omitempty"` @@ -7335,6 +7423,9 @@ type ModelSwitchToResult struct { Message *string `json:"message,omitempty"` // Currently active model identifier after the switch ModelID *string `json:"modelId,omitempty"` + // Authoritative model and Auto preference state after an immediate switch. For deferred + // switches this remains the current state until the queued change drains. + ModelState *CurrentModel `json:"modelState,omitempty"` // Persistence failure encountered after applying the model switch. PersistenceError *string `json:"persistenceError,omitempty"` // Lifecycle result for the requested switch @@ -10316,6 +10407,12 @@ type RuntimeShutdownResult struct { type SandboxConfig struct { // Whether to auto-add the current working directory to readwritePaths. Default: true. AddCurrentWorkingDirectory *bool `json:"addCurrentWorkingDirectory,omitempty"` + // Whether the agent may request that an individual command run outside the sandbox, which + // the host then approves or denies through the usual permission flow. A host capability + // flag rather than part of the policy: it is stripped from the effective spawn policy and + // only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this + // object: omitting it offers no bypass. Default: false (opt-in). + AllowBypass *bool `json:"allowBypass,omitempty"` // Whether to auto-grant read access to tool directories discovered on PATH and in toolchain // environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common // developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the @@ -10332,6 +10429,27 @@ type SandboxConfig struct { Auth *SandboxConfigAuth `json:"auth,omitempty"` // Whether sandboxing is enabled for the session. Enabled bool `json:"enabled"` + // The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + // Internal: ManagedLspRoutingLocked is part of the SDK's internal API surface and is not + // intended for external use. + ManagedLspRoutingLocked *bool `json:"managedLspRoutingLocked,omitempty"` + // Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local + // opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at + // the administrator instead of a setting the next managed merge would override, and it is + // ignored when comparing two configs for change. Only the managed merge may set it; a + // caller-supplied value is stripped. + // Internal: ManagedMCPRoutingLocked is part of the SDK's internal API surface and is not + // intended for external use. + ManagedMCPRoutingLocked *bool `json:"managedMcpRoutingLocked,omitempty"` + // Whether language servers the session launches are confined by the sandbox. Only an + // explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by + // default; set to false to opt out). + SandboxLspServers *bool `json:"sandboxLspServers,omitempty"` + // Whether MCP servers the session launches are confined by the sandbox. Only an explicit + // `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and + // `enabled` are always read together. Ignored while `enabled` is false. Default: true + // (enabled by default; set to false to opt out). + SandboxMCPServers *bool `json:"sandboxMcpServers,omitempty"` // User-managed sandbox policy fragment merged into the auto-discovered base policy. UserPolicy *SandboxConfigUserPolicy `json:"userPolicy,omitempty"` } @@ -11994,6 +12112,8 @@ type SessionOpenOptions struct { AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` // Whether ask_user is explicitly disabled. AskUserDisabled *bool `json:"askUserDisabled,omitempty"` + // OAuth Client ID Metadata Document URL used by this host for MCP authorization. + AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` // Initial authentication info for the session. AuthInfo AuthInfo `json:"authInfo,omitempty"` // Allowlist of available tool names. @@ -14072,6 +14192,102 @@ type SubagentSettingsEntry struct { ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` } +// Public owner attribution for a client-owned task. Identifiers are opaque and never +// authorize requests. +// Experimental: TaskClientOwner is part of an experimental API and may change or be removed. +type TaskClientOwner struct { + // ISO 8601 timestamp when the bound join disconnected + DisconnectedAt *time.Time `json:"disconnectedAt,omitempty"` + // Display-only owner name + DisplayName *string `json:"displayName,omitempty"` + // Opaque identity of the currently or most recently bound session join + JoinID string `json:"joinId"` + // Class of the task owner + Kind TaskClientOwnerKind `json:"kind"` + // Opaque session-scoped participant identity + ParticipantID string `json:"participantId"` + // Whether this task's bound join is currently connected + Presence TaskClientOwnerPresence `json:"presence"` + // Display-only owner source + Source *string `json:"source,omitempty"` +} + +// Progress or terminal update for a client-owned task. +// Experimental: TaskClientUpdate is part of an experimental API and may change or be +// removed. +type TaskClientUpdate interface { + taskClientUpdate() + Kind() TaskClientUpdateKind +} + +type RawTaskClientUpdateData struct { + Discriminator TaskClientUpdateKind + Raw json.RawMessage +} + +func (RawTaskClientUpdateData) taskClientUpdate() {} +func (r RawTaskClientUpdateData) Kind() TaskClientUpdateKind { + return r.Discriminator +} + +// Reports terminal cancellation after external work stopped. +type TaskClientUpdateCancelled struct { + // Optional final progress message + Message *string `json:"message,omitempty"` + // Optional human-readable cancellation reason + Reason *string `json:"reason,omitempty"` +} + +func (TaskClientUpdateCancelled) taskClientUpdate() {} +func (TaskClientUpdateCancelled) Kind() TaskClientUpdateKind { + return TaskClientUpdateKindCancelled +} + +// Reports successful terminal completion. +type TaskClientUpdateCompleted struct { + // Optional final progress message + Message *string `json:"message,omitempty"` + // Optional opaque successful terminal result + Result any `json:"result,omitempty"` +} + +func (TaskClientUpdateCompleted) taskClientUpdate() {} +func (TaskClientUpdateCompleted) Kind() TaskClientUpdateKind { + return TaskClientUpdateKindCompleted +} + +// Reports terminal failure. +type TaskClientUpdateFailed struct { + // Optional owner-supplied terminal failure code + Code *string `json:"code,omitempty"` + // Human-readable terminal failure message + Error string `json:"error"` + // Optional final progress message + Message *string `json:"message,omitempty"` +} + +func (TaskClientUpdateFailed) taskClientUpdate() {} +func (TaskClientUpdateFailed) Kind() TaskClientUpdateKind { + return TaskClientUpdateKindFailed +} + +// Publishes nonterminal progress for a running or idle client task. +type TaskClientUpdateProgress struct { + // Optional progress message appended to recent activity when nonempty + Message *string `json:"message,omitempty"` + // Optional completion percentage; null clears the current percentage + Percentage **float64 `json:"percentage,omitempty"` + // Optional progress phase; null clears the current phase + Phase **string `json:"phase,omitempty"` + // Optional active status transition + Status *TaskClientActiveStatus `json:"status,omitempty"` +} + +func (TaskClientUpdateProgress) taskClientUpdate() {} +func (TaskClientUpdateProgress) Kind() TaskClientUpdateKind { + return TaskClientUpdateKindProgress +} + // Task completion notification with summary from the agent // Experimental: TaskCompleteData is part of an experimental API and may change or be // removed. @@ -14111,7 +14327,7 @@ type TaskCompletionDecision struct { ReviewerResultMeta any `json:"reviewerResultMeta,omitempty"` } -// Tracked task union returned by task APIs, containing either an agent task or a shell task. +// Tracked task union returned by task APIs, containing an agent, client, or shell task. // Experimental: TaskInfo is part of an experimental API and may change or be removed. type TaskInfo interface { taskInfo() @@ -14180,6 +14396,58 @@ func (TaskAgentInfo) Type() TaskInfoType { return TaskInfoTypeAgent } +// Tracked client-owned task metadata. +// Experimental: TaskClientInfo is part of an experimental API and may change or be removed. +type TaskClientInfo struct { + // ISO 8601 timestamp when the current active segment started + ActiveStartedAt *time.Time `json:"activeStartedAt,omitempty"` + // Accumulated active execution time in milliseconds + ActiveTimeMs int64 `json:"activeTimeMs"` + // Whether the currently bound owner can receive a cancellation request + CanCancel bool `json:"canCancel"` + // Human-readable reason for terminal cancellation + CancellationReason *string `json:"cancellationReason,omitempty"` + // Owner-scoped registration and reclaim key + ClientTaskID string `json:"clientTaskId"` + // ISO 8601 timestamp when the task reached a terminal status + CompletedAt *time.Time `json:"completedAt,omitempty"` + // Task description + Description string `json:"description"` + // Optional task display name + DisplayName *string `json:"displayName,omitempty"` + // Human-readable terminal failure message + Error *string `json:"error,omitempty"` + // Optional owner-supplied terminal failure code + ErrorCode *string `json:"errorCode,omitempty"` + // Execution mode, which is always background for client-owned tasks + ExecutionMode TaskClientExecutionMode `json:"executionMode"` + // Canonical runtime-generated task identifier + ID string `json:"id"` + // ISO 8601 timestamp when the connected owner entered idle status + IdleSince *time.Time `json:"idleSince,omitempty"` + // ISO 8601 timestamp of the most recent orphan transition + OrphanedAt *time.Time `json:"orphanedAt,omitempty"` + // Public attribution and presence for the task owner + Owner TaskClientOwner `json:"owner"` + // ISO 8601 timestamp of the most recent successful reclaim + ReclaimedAt *time.Time `json:"reclaimedAt,omitempty"` + // Opaque successful terminal result supplied by the task owner + Result any `json:"result,omitempty"` + // Sequence number of the latest accepted owner update + Sequence int64 `json:"sequence"` + // ISO 8601 timestamp when the task started + StartedAt time.Time `json:"startedAt"` + // Client task lifecycle status + Status TaskClientStatus `json:"status"` + // ISO 8601 timestamp of the latest accepted lifecycle change + UpdatedAt time.Time `json:"updatedAt"` +} + +func (TaskClientInfo) taskInfo() {} +func (TaskClientInfo) Type() TaskInfoType { + return TaskInfoTypeClient +} + // Tracked shell task metadata, including ID, command, status, timing, attachment/execution // mode, log path, and PID. // Experimental: TaskShellInfo is part of an experimental API and may change or be removed. @@ -14221,6 +14489,8 @@ type TaskList struct { Tasks []TaskInfo `json:"tasks"` } +// Progress information for the task, discriminated by type. Returns null when no task with +// this ID is currently tracked. // Experimental: TaskProgress is part of an experimental API and may change or be removed. type TaskProgress interface { taskProgress() @@ -14253,6 +14523,31 @@ func (TaskAgentProgress) Type() TaskProgressType { return TaskProgressTypeAgent } +// Generic progress for a client-owned task. +// Experimental: TaskClientProgress is part of an experimental API and may change or be +// removed. +type TaskClientProgress struct { + // Most recent nonempty progress message + LastMessage *string `json:"lastMessage,omitempty"` + // Current completion percentage from zero through one hundred + Percentage *float64 `json:"percentage,omitempty"` + // Current owner-defined progress phase + Phase *string `json:"phase,omitempty"` + // Recent server-timestamped progress messages + RecentActivity []TaskProgressLine `json:"recentActivity"` + // Sequence number of the latest accepted owner update + Sequence int64 `json:"sequence"` + // Current client task lifecycle status + Status TaskClientStatus `json:"status"` + // ISO 8601 timestamp of the latest accepted lifecycle change + UpdatedAt time.Time `json:"updatedAt"` +} + +func (TaskClientProgress) taskProgress() {} +func (TaskClientProgress) Type() TaskProgressType { + return TaskProgressTypeClient +} + // Progress snapshot for a shell task, with recent stdout/stderr output and optional process // ID. // Experimental: TaskShellProgress is part of an experimental API and may change or be @@ -14356,6 +14651,36 @@ type TasksPromoteToBackgroundResult struct { type TasksRefreshResult struct { } +// Registers or reclaims a client-owned task. +// Experimental: TasksRegisterRequest is part of an experimental API and may change or be +// removed. +type TasksRegisterRequest struct { + // Whether the owner supports runtime cancellation requests + Cancellable bool `json:"cancellable"` + // Owner-scoped idempotency key used for registration and reclaim + ClientTaskID string `json:"clientTaskId"` + // Human-readable description of the external work + Description string `json:"description"` + // Optional short display name for the external work + DisplayName *string `json:"displayName,omitempty"` + // Expected current sequence for idempotent registration or orphan reclaim + ExpectedSequence *int64 `json:"expectedSequence,omitempty"` + // Task kind + Type TaskClientType `json:"type"` +} + +// Result of registering or reclaiming a client-owned task. +// Experimental: TasksRegisterResult is part of an experimental API and may change or be +// removed. +type TasksRegisterResult struct { + // True only when this invocation created a new task + Created bool `json:"created"` + // True only when this invocation reclaimed an orphaned task + Reclaimed bool `json:"reclaimed"` + // Authoritative registered or reclaimed task + Task TaskClientInfo `json:"task"` +} + // Identifier of the completed or cancelled task to remove from tracking. // Experimental: TasksRemoveRequest is part of an experimental API and may change or be // removed. @@ -14420,6 +14745,30 @@ type TasksStartAgentResult struct { AgentID string `json:"agentId"` } +// Updates a client-owned task. +// Experimental: TasksUpdateRequest is part of an experimental API and may change or be +// removed. +type TasksUpdateRequest struct { + // Canonical runtime-generated task identifier + ID string `json:"id"` + // Owner update sequence to apply + Sequence int64 `json:"sequence"` + // Progress or terminal update payload + Update TaskClientUpdate `json:"update"` +} + +// Result of publishing a client-owned task update. +// Experimental: TasksUpdateResult is part of an experimental API and may change or be +// removed. +type TasksUpdateResult struct { + // Whether this invocation changed task state + Applied bool `json:"applied"` + // Whether this invocation repeated the latest accepted update + Duplicate bool `json:"duplicate"` + // Authoritative task after processing the update + Task TaskClientInfo `json:"task"` +} + // Wait until all in-flight background tasks (agents + shells) and any follow-up turns // scheduled by their completions have settled. Returns when the runtime is fully drained or // after an internal timeout (default 10 minutes; configurable via @@ -16435,6 +16784,18 @@ const ( CatalogUnsafeRetrievalReasonRedirectToBlockedAddress CatalogUnsafeRetrievalReason = "redirect-to-blocked-address" ) +// Why the runtime requests client-task cancellation. +// Experimental: ClientTaskCancelReason is part of an experimental API and may change or be +// removed. +type ClientTaskCancelReason string + +const ( + // A caller requested task cancellation. + ClientTaskCancelReasonCancelRequested ClientTaskCancelReason = "cancel_requested" + // The session is shutting down. + ClientTaskCancelReasonSessionShutdown ClientTaskCancelReason = "session_shutdown" +) + // Whether a pending slash-command invocation effect was applied or cancelled by the host. // Experimental: CommandsInvocationEffectOutcome is part of an experimental API and may // change or be removed. @@ -17811,6 +18172,22 @@ const ( ModelPolicyStateUnconfigured ModelPolicyState = "unconfigured" ) +// Whether the requested preference was already effective or was accepted for later +// transactional activation. +// Experimental: ModelSwitchAutoTierStatus is part of an experimental API and may change or +// be removed. +type ModelSwitchAutoTierStatus string + +const ( + // The request was accepted but has not committed. A later user turn using the `auto` model + // must mint and validate the replacement before it becomes effective. + ModelSwitchAutoTierStatusPending ModelSwitchAutoTierStatus = "pending" + // The requested preference is already effective. No activation is pending for it, although + // this request may have cancelled an earlier unclaimed preference reported in + // `supersededAutoTier`. + ModelSwitchAutoTierStatusUnchanged ModelSwitchAutoTierStatus = "unchanged" +) + // Why the binary data is absent: it exceeded the inline size limit, or its asset was // unavailable // Experimental: OmittedBinaryOmittedReason is part of an experimental API and may change or @@ -18969,6 +19346,89 @@ const ( SubagentSettingsEntryContextTierLongContext SubagentSettingsEntryContextTier = "long_context" ) +// Active status a client owner may publish with a progress update. +// Experimental: TaskClientActiveStatus is part of an experimental API and may change or be +// removed. +type TaskClientActiveStatus string + +const ( + // The external owner is connected but waiting. + TaskClientActiveStatusIdle TaskClientActiveStatus = "idle" + // The external owner is actively working. + TaskClientActiveStatusRunning TaskClientActiveStatus = "running" +) + +// Client-owned tasks always execute outside the runtime in background mode. +// Experimental: TaskClientExecutionMode is part of an experimental API and may change or be +// removed. +type TaskClientExecutionMode string + +const ( + TaskClientExecutionModeBackground TaskClientExecutionMode = "background" +) + +// Connection class owning a client task. +// Experimental: TaskClientOwnerKind is part of an experimental API and may change or be +// removed. +type TaskClientOwnerKind string + +const ( + // A discovered extension connection owns the task. + TaskClientOwnerKindExtension TaskClientOwnerKind = "extension" + // A generic SDK connection owns the task. + TaskClientOwnerKindSDK TaskClientOwnerKind = "sdk" +) + +// Presence of the task's bound join. +// Experimental: TaskClientOwnerPresence is part of an experimental API and may change or be +// removed. +type TaskClientOwnerPresence string + +const ( + // The bound session join is connected. + TaskClientOwnerPresenceConnected TaskClientOwnerPresence = "connected" + // The bound session join is disconnected. + TaskClientOwnerPresenceDisconnected TaskClientOwnerPresence = "disconnected" +) + +// Lifecycle status of a client-owned task. +// Experimental: TaskClientStatus is part of an experimental API and may change or be +// removed. +type TaskClientStatus string + +const ( + // The owner reported or confirmed cancellation. + TaskClientStatusCancelled TaskClientStatus = "cancelled" + // The owner reported successful completion. + TaskClientStatusCompleted TaskClientStatus = "completed" + // The owner reported failure. + TaskClientStatusFailed TaskClientStatus = "failed" + // The external owner is connected but waiting. + TaskClientStatusIdle TaskClientStatus = "idle" + // The bound owner join disappeared; external executor state is unknown. + TaskClientStatusOrphaned TaskClientStatus = "orphaned" + // The external owner is actively working. + TaskClientStatusRunning TaskClientStatus = "running" +) + +// Discriminator for a client-owned task. +// Experimental: TaskClientType is part of an experimental API and may change or be removed. +type TaskClientType string + +const ( + TaskClientTypeClient TaskClientType = "client" +) + +// Kind discriminator for TaskClientUpdate. +type TaskClientUpdateKind string + +const ( + TaskClientUpdateKindCancelled TaskClientUpdateKind = "cancelled" + TaskClientUpdateKindCompleted TaskClientUpdateKind = "completed" + TaskClientUpdateKindFailed TaskClientUpdateKind = "failed" + TaskClientUpdateKindProgress TaskClientUpdateKind = "progress" +) + // Semantic result of evaluating a task completion request // Experimental: TaskCompletionOutcome is part of an experimental API and may change or be // removed. @@ -19000,16 +19460,31 @@ const ( type TaskInfoType string const ( - TaskInfoTypeAgent TaskInfoType = "agent" - TaskInfoTypeShell TaskInfoType = "shell" + TaskInfoTypeAgent TaskInfoType = "agent" + TaskInfoTypeClient TaskInfoType = "client" + TaskInfoTypeShell TaskInfoType = "shell" +) + +// Closed set of public task kinds a connection can negotiate. +// Experimental: TaskKind is part of an experimental API and may change or be removed. +type TaskKind string + +const ( + // Runtime-owned background agent task. + TaskKindAgent TaskKind = "agent" + // Client-owned externally executed task. + TaskKindClient TaskKind = "client" + // Runtime-owned shell task. + TaskKindShell TaskKind = "shell" ) // Type discriminator for TaskProgress. type TaskProgressType string const ( - TaskProgressTypeAgent TaskProgressType = "agent" - TaskProgressTypeShell TaskProgressType = "shell" + TaskProgressTypeAgent TaskProgressType = "agent" + TaskProgressTypeClient TaskProgressType = "client" + TaskProgressTypeShell TaskProgressType = "shell" ) // Whether the shell runs inside a managed PTY session or as an independent background @@ -19688,6 +20163,32 @@ func (a *ServerLlmInferenceAPI) SetProvider(ctx context.Context) (*LlmInferenceS // removed. type ServerManagedSettingsAPI serverAPI +// ClearCache force-refreshes enterprise managed settings for every account: wipes the +// persistent server-policy cache (the whole `/managed-settings` directory) and +// drops this runtime process's in-memory retained server policy. It does not itself fetch +// policy — the effect is that the next time a session resolves managed settings for an +// account, that resolution re-fetches the account's org policy from the network instead of +// serving a cached response. Note that `managedSettings.read` returns only device/MDM +// settings and never triggers the account server-policy fetch, so a host implementing "sync +// account policy" should start a fresh session resolution rather than treat a subsequent +// `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out +// performs, broadened from the one signing-out account to all of them; device/MDM layers +// describe the machine, not the account, and are left untouched. Rejects if the on-disk +// cache cannot be removed. +// +// RPC method: managedSettings.clearCache. +func (a *ServerManagedSettingsAPI) ClearCache(ctx context.Context) (*ManagedSettingsClearCacheResult, error) { + raw, err := a.client.Request(ctx, "managedSettings.clearCache", nil) + if err != nil { + return nil, err + } + var result ManagedSettingsClearCacheResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Read discovers device-managed settings from production MDM and managed-file sources, // validates them against the runtime-owned managed-settings schema, and returns the // canonical JSON without requiring a session. @@ -23843,13 +24344,15 @@ func (a *ModeAPI) Set(ctx context.Context, params *ModeSetRequest) (*ModeSetResu // Experimental: ModelAPI contains experimental APIs that may change or be removed. type ModelAPI sessionAPI -// GetCurrent gets the currently selected model for the session. +// GetCurrent gets the session's authoritative model snapshot, including the committed Auto +// preference and any newer unclaimed Auto preference waiting for a future user turn. // // RPC method: session.model.getCurrent. // -// Returns: The currently selected model, reasoning effort, and context tier for the -// session. The context tier reflects `Session.getContextTier()`, restored from the session -// journal on resume. +// Returns: The session's authoritative model snapshot. Auto preference fields are +// configuration for the virtual `auto` model and do not change the selected model +// identifier. The context tier reflects `Session.getContextTier()`, restored from the +// session journal on resume. func (a *ModelAPI) GetCurrent(ctx context.Context) (*CurrentModel, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request(ctx, "session.model.getCurrent", req) @@ -23920,6 +24423,40 @@ func (a *ModelAPI) SetReasoningEffort(ctx context.Context, params *ModelSetReaso return &result, nil } +// SwitchAutoTier requests an Auto preference change without changing the session's selected +// model. The latest unclaimed request wins; the runtime commits it only after a later +// prompt using the `auto` model mints a usable model and token pair. A `pending` response +// confirms that the request was accepted, not that it committed. Observe eventual success +// through `session.model_change`, failure through the ephemeral +// `session.auto_tier_switch_failed` event, or current unclaimed state through +// `session.model.getCurrent`. +// +// RPC method: session.model.switchAutoTier. +// +// Parameters: An Auto preference request for the session. This updates Auto configuration +// only; it does not change the selected model to `auto`. +// +// Returns: Immediate acknowledgement and Auto preference snapshot after a switch request. +// This result never implies that a pending preference committed. +func (a *ModelAPI) SwitchAutoTier(ctx context.Context, params *ModelSwitchAutoTierRequest) (*ModelSwitchAutoTierResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["autoTier"] = params.AutoTier + if params.Source != nil { + req["source"] = *params.Source + } + } + raw, err := a.client.Request(ctx, "session.model.switchAutoTier", req) + if err != nil { + return nil, err + } + var result ModelSwitchAutoTierResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // SwitchTo switches the session to a model and optional reasoning configuration. // // RPC method: session.model.switchTo. @@ -23931,6 +24468,9 @@ func (a *ModelAPI) SetReasoningEffort(ctx context.Context, params *ModelSetReaso func (a *ModelAPI) SwitchTo(ctx context.Context, params *ModelSwitchToRequest) (*ModelSwitchToResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { + if params.AutoTier != nil { + req["autoTier"] = *params.AutoTier + } if params.CompactionDecision != nil { req["compactionDecision"] = *params.CompactionDecision } @@ -25807,6 +26347,39 @@ func (a *TasksAPI) Refresh(ctx context.Context) (*TasksRefreshResult, error) { return &result, nil } +// Registers a client-owned task, or reclaims an orphaned task belonging to the same +// extension principal. +// +// RPC method: session.tasks.register. +// +// Parameters: Registers or reclaims a client-owned task. +// +// Returns: Result of registering or reclaiming a client-owned task. +func (a *TasksAPI) Register(ctx context.Context, params *TasksRegisterRequest) (*TasksRegisterResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["cancellable"] = params.Cancellable + req["clientTaskId"] = params.ClientTaskID + req["description"] = params.Description + if params.DisplayName != nil { + req["displayName"] = *params.DisplayName + } + if params.ExpectedSequence != nil { + req["expectedSequence"] = *params.ExpectedSequence + } + req["type"] = params.Type + } + raw, err := a.client.Request(ctx, "session.tasks.register", req) + if err != nil { + return nil, err + } + var result TasksRegisterResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Removes a completed or cancelled background task from tracking. // // RPC method: session.tasks.remove. @@ -25892,6 +26465,31 @@ func (a *TasksAPI) StartAgent(ctx context.Context, params *TasksStartAgentReques return &result, nil } +// Update publishes generic progress or a terminal outcome for a client-owned task. +// +// RPC method: session.tasks.update. +// +// Parameters: Updates a client-owned task. +// +// Returns: Result of publishing a client-owned task update. +func (a *TasksAPI) Update(ctx context.Context, params *TasksUpdateRequest) (*TasksUpdateResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["id"] = params.ID + req["sequence"] = params.Sequence + req["update"] = params.Update + } + raw, err := a.client.Request(ctx, "session.tasks.update", req) + if err != nil { + return nil, err + } + var result TasksUpdateResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // WaitForPending waits for all in-flight background tasks and any follow-up turns to settle. // // RPC method: session.tasks.waitForPending. @@ -28479,12 +29077,26 @@ type SessionFSHandler interface { WriteFile(request *SessionFSWriteFileRequest) (*SessionFSError, error) } +// Experimental: TasksHandler contains experimental APIs that may change or be removed. +type TasksHandler interface { + // Cancel asks the client currently bound to a client-owned session task to confirm that its + // external work stopped. + // + // RPC method: tasks.cancel. + // + // Parameters: Runtime-to-owner cancellation request for a client-owned task. + // + // Returns: Whether the client authoritatively confirmed its external work stopped. + Cancel(request *ClientTaskCancelRequest) (*ClientTaskCancelResult, error) +} + // ClientSessionAPIHandlers provides all client session API handler groups for a session. type ClientSessionAPIHandlers struct { Canvas CanvasHandler Factory FactoryHandler ProviderToken ProviderTokenHandler SessionFS SessionFSHandler + Tasks TasksHandler } func clientSessionHandlerError(err error) *jsonrpc2.Error { @@ -28862,6 +29474,25 @@ func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func( } return raw, nil }) + client.SetRequestHandler("tasks.cancel", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request ClientTaskCancelRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + handlers := getHandlers(request.SessionID) + if handlers == nil || handlers.Tasks == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No tasks handler registered for session: %s", request.SessionID)} + } + result, err := handlers.Tasks.Cancel(&request) + if err != nil { + return nil, clientSessionHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) } // Experimental: ExtensionLaunchProviderHandler contains experimental APIs that may change diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 9242a889a8..13d190be22 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -5381,6 +5381,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { AgentContext *string `json:"agentContext,omitempty"` AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` AskUserDisabled *bool `json:"askUserDisabled,omitempty"` + AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` AuthInfo json.RawMessage `json:"authInfo,omitempty"` AvailableTools []string `json:"availableTools,omitzero"` Capi *CapiSessionOptions `json:"capi,omitempty"` @@ -5457,6 +5458,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.AgentContext = raw.AgentContext r.AllowAllMCPServerInstructions = raw.AllowAllMCPServerInstructions r.AskUserDisabled = raw.AskUserDisabled + r.AuthClientIDMetadataURL = raw.AuthClientIDMetadataURL if raw.AuthInfo != nil { value, err := unmarshalAuthInfo(raw.AuthInfo) if err != nil { @@ -5945,6 +5947,103 @@ func (r SlashCommandTextResult) MarshalJSON() ([]byte, error) { }) } +func unmarshalTaskClientUpdate(data []byte) (TaskClientUpdate, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind TaskClientUpdateKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case TaskClientUpdateKindCancelled: + var d TaskClientUpdateCancelled + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case TaskClientUpdateKindCompleted: + var d TaskClientUpdateCompleted + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case TaskClientUpdateKindFailed: + var d TaskClientUpdateFailed + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case TaskClientUpdateKindProgress: + var d TaskClientUpdateProgress + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawTaskClientUpdateData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawTaskClientUpdateData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r TaskClientUpdateCancelled) MarshalJSON() ([]byte, error) { + type alias TaskClientUpdateCancelled + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r TaskClientUpdateCompleted) MarshalJSON() ([]byte, error) { + type alias TaskClientUpdateCompleted + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r TaskClientUpdateFailed) MarshalJSON() ([]byte, error) { + type alias TaskClientUpdateFailed + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r TaskClientUpdateProgress) MarshalJSON() ([]byte, error) { + type alias TaskClientUpdateProgress + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + func unmarshalTaskInfo(data []byte) (TaskInfo, error) { if string(data) == "null" { return nil, nil @@ -5964,6 +6063,12 @@ func unmarshalTaskInfo(data []byte) (TaskInfo, error) { return nil, err } return &d, nil + case TaskInfoTypeClient: + var d TaskClientInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case TaskInfoTypeShell: var d TaskShellInfo if err := json.Unmarshal(data, &d); err != nil { @@ -5997,6 +6102,17 @@ func (r TaskAgentInfo) MarshalJSON() ([]byte, error) { }) } +func (r TaskClientInfo) MarshalJSON() ([]byte, error) { + type alias TaskClientInfo + return json.Marshal(struct { + Type TaskInfoType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r TaskShellInfo) MarshalJSON() ([]byte, error) { type alias TaskShellInfo return json.Marshal(struct { @@ -6048,6 +6164,12 @@ func unmarshalTaskProgress(data []byte) (TaskProgress, error) { return nil, err } return &d, nil + case TaskProgressTypeClient: + var d TaskClientProgress + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case TaskProgressTypeShell: var d TaskShellProgress if err := json.Unmarshal(data, &d); err != nil { @@ -6081,6 +6203,17 @@ func (r TaskAgentProgress) MarshalJSON() ([]byte, error) { }) } +func (r TaskClientProgress) MarshalJSON() ([]byte, error) { + type alias TaskClientProgress + return json.Marshal(struct { + Type TaskProgressType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r TaskShellProgress) MarshalJSON() ([]byte, error) { type alias TaskShellProgress return json.Marshal(struct { @@ -6146,6 +6279,28 @@ func (r *TasksPromoteCurrentToBackgroundResult) UnmarshalJSON(data []byte) error return nil } +func (r *TasksUpdateRequest) UnmarshalJSON(data []byte) error { + type rawTasksUpdateRequest struct { + ID string `json:"id"` + Sequence int64 `json:"sequence"` + Update json.RawMessage `json:"update"` + } + var raw rawTasksUpdateRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.ID = raw.ID + r.Sequence = raw.Sequence + if raw.Update != nil { + value, err := unmarshalTaskClientUpdate(raw.Update) + if err != nil { + return err + } + r.Update = value + } + return nil +} + func (r *ToolResultExpanded) UnmarshalJSON(data []byte) error { type rawToolResultExpanded struct { BinaryResultsForLlm []ExternalToolTextResultForLlmBinaryResultsForLlm `json:"binaryResultsForLlm,omitzero"` diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index bbe9fbc74b..a220b79ad5 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -389,6 +389,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionAutoTierSwitchFailed: + var d SessionAutoTierSwitchFailedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionBackgroundTasksChanged: var d SessionBackgroundTasksChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -563,6 +569,18 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionMCPServerNeedsReconnect: + var d SessionMCPServerNeedsReconnectData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeSessionMCPServerRemoved: + var d SessionMCPServerRemovedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionMCPServersLoaded: var d SessionMCPServersLoadedData if err := json.Unmarshal(raw.Data, &d); err != nil { diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 63709921d8..ccddffb360 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -128,6 +128,7 @@ const ( // that may change or be removed. SessionEventTypeSessionAutoModeResolved SessionEventType = "session.auto_mode_resolved" SessionEventTypeSessionAutopilotObjectiveChanged SessionEventType = "session.autopilot_objective_changed" + SessionEventTypeSessionAutoTierSwitchFailed SessionEventType = "session.auto_tier_switch_failed" SessionEventTypeSessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" // Experimental: SessionEventTypeSessionBinaryAsset identifies an experimental event that // may change or be removed. @@ -185,6 +186,8 @@ const ( // Experimental: SessionEventTypeSessionManagedSettingsResolved identifies an experimental // event that may change or be removed. SessionEventTypeSessionManagedSettingsResolved SessionEventType = "session.managed_settings_resolved" + SessionEventTypeSessionMCPServerNeedsReconnect SessionEventType = "session.mcp_server_needs_reconnect" + SessionEventTypeSessionMCPServerRemoved SessionEventType = "session.mcp_server_removed" SessionEventTypeSessionMCPServersLoaded SessionEventType = "session.mcp_servers_loaded" SessionEventTypeSessionMCPServerStatusChanged SessionEventType = "session.mcp_server_status_changed" SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed" @@ -306,6 +309,21 @@ type PromptCacheBreakData struct { func (*PromptCacheBreakData) sessionEventData() {} func (*PromptCacheBreakData) Type() SessionEventType { return SessionEventTypePromptCacheBreak } +// A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. +type SessionAutoTierSwitchFailedData struct { + // Auto preference that remains effective after the failed request. + EffectiveAutoTier *AutoTier `json:"effectiveAutoTier,omitempty"` + // Low-cardinality failure outcome reported by Auto resolution. + Reason AutoTierSwitchFailureReason `json:"reason"` + // Auto preference that failed to activate, or null when returning to provider-default routing failed. + RequestedAutoTier *AutoTier `json:"requestedAutoTier"` +} + +func (*SessionAutoTierSwitchFailedData) sessionEventData() {} +func (*SessionAutoTierSwitchFailedData) Type() SessionEventType { + return SessionEventTypeSessionAutoTierSwitchFailed +} + // Agent intent description for current activity or plan type AssistantIntentData struct { // Short description of what the agent is currently doing or planning to do @@ -1577,12 +1595,16 @@ func (*ModelCallStartData) Type() SessionEventType { return SessionEventTypeMode // Model change details including previous and new model identifiers type SessionModelChangeData struct { + // Committed Auto preference after the model configuration change, when applicable. + AutoTier *AutoTier `json:"autoTier,omitempty"` // Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. Cause *string `json:"cause,omitempty"` // Context tier after the model change; null explicitly clears a previously selected tier ContextTier *ContextTier `json:"contextTier,omitempty"` // Newly selected model identifier NewModel string `json:"newModel"` + // Previously committed Auto preference, when one was explicitly selected. + PreviousAutoTier *AutoTier `json:"previousAutoTier,omitempty"` // Model that was previously selected, if any PreviousModel *string `json:"previousModel,omitempty"` // Reasoning effort level before the model change, if applicable @@ -1822,6 +1844,28 @@ func (*SessionExtensionsLoadedData) Type() SessionEventType { return SessionEventTypeSessionExtensionsLoaded } +// Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. +type SessionMCPServerNeedsReconnectData struct { + // Name of the MCP server that needs to reconnect + ServerName string `json:"serverName"` +} + +func (*SessionMCPServerNeedsReconnectData) sessionEventData() {} +func (*SessionMCPServerNeedsReconnectData) Type() SessionEventType { + return SessionEventTypeSessionMCPServerNeedsReconnect +} + +// Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. +type SessionMCPServerRemovedData struct { + // Name of the MCP server that was removed from the graph + ServerName string `json:"serverName"` +} + +func (*SessionMCPServerRemovedData) sessionEventData() {} +func (*SessionMCPServerRemovedData) Type() SessionEventType { + return SessionEventTypeSessionMCPServerRemoved +} + // Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error. type SessionMCPServerStatusChangedData struct { // Error message if the server entered a failed state @@ -5119,6 +5163,20 @@ const ( AutopilotObjectiveChangedStatusPaused AutopilotObjectiveChangedStatus = "paused" ) +// Terminal reason an Auto preference activation failed. +type AutoTierSwitchFailureReason string + +const ( + // The candidate model was rejected by model policy. + AutoTierSwitchFailureReasonPolicyRejected AutoTierSwitchFailureReason = "policy_rejected" + // The Auto routing request failed or returned an unusable response. + AutoTierSwitchFailureReasonRequestFailed AutoTierSwitchFailureReason = "request_failed" + // The runtime could not prepare the Auto routing request. + AutoTierSwitchFailureReasonSetupFailed AutoTierSwitchFailureReason = "setup_failed" + // The provider does not support Auto routing. + AutoTierSwitchFailureReasonUnsupported AutoTierSwitchFailureReason = "unsupported" +) + // Binary result type discriminator. Use "image" for images and "resource" for other binary data. type BinaryAssetReferenceType string diff --git a/go/types.go b/go/types.go index 772202a0bc..02ca391338 100644 --- a/go/types.go +++ b/go/types.go @@ -2514,6 +2514,7 @@ type ModelInfo struct { Capabilities ModelCapabilities `json:"capabilities"` Policy *ModelPolicy `json:"policy,omitempty"` Billing *ModelBilling `json:"billing,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` SupportedReasoningEfforts []string `json:"supportedReasoningEfforts,omitempty"` DefaultReasoningEffort string `json:"defaultReasoningEffort,omitempty"` } diff --git a/go/zsession_events.go b/go/zsession_events.go index b5f6fa34f0..8ac49affba 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -73,6 +73,7 @@ type ( AutoModeSwitchResponse = rpc.AutoModeSwitchResponse AutopilotObjectiveChangedOperation = rpc.AutopilotObjectiveChangedOperation AutopilotObjectiveChangedStatus = rpc.AutopilotObjectiveChangedStatus + AutoTierSwitchFailureReason = rpc.AutoTierSwitchFailureReason BinaryAssetReference = rpc.BinaryAssetReference BinaryAssetReferenceType = rpc.BinaryAssetReferenceType BinaryAssetType = rpc.BinaryAssetType @@ -263,6 +264,7 @@ type ( ScheduleOrigin = rpc.ScheduleOrigin SessionAutoModeResolvedData = rpc.SessionAutoModeResolvedData SessionAutopilotObjectiveChangedData = rpc.SessionAutopilotObjectiveChangedData + SessionAutoTierSwitchFailedData = rpc.SessionAutoTierSwitchFailedData SessionBackgroundTasksChangedData = rpc.SessionBackgroundTasksChangedData SessionBinaryAssetData = rpc.SessionBinaryAssetData SessionCanvasClosedData = rpc.SessionCanvasClosedData @@ -298,6 +300,8 @@ type ( SessionLimitsExhaustedResponseAction = rpc.SessionLimitsExhaustedResponseAction SessionManagedSettingsEnforcedData = rpc.SessionManagedSettingsEnforcedData SessionManagedSettingsResolvedData = rpc.SessionManagedSettingsResolvedData + SessionMCPServerNeedsReconnectData = rpc.SessionMCPServerNeedsReconnectData + SessionMCPServerRemovedData = rpc.SessionMCPServerRemovedData SessionMCPServersLoadedData = rpc.SessionMCPServersLoadedData SessionMCPServerStatusChangedData = rpc.SessionMCPServerStatusChangedData SessionMode = rpc.SessionMode @@ -487,6 +491,10 @@ const ( AutopilotObjectiveChangedStatusCapReached = rpc.AutopilotObjectiveChangedStatusCapReached AutopilotObjectiveChangedStatusCompleted = rpc.AutopilotObjectiveChangedStatusCompleted AutopilotObjectiveChangedStatusPaused = rpc.AutopilotObjectiveChangedStatusPaused + AutoTierSwitchFailureReasonPolicyRejected = rpc.AutoTierSwitchFailureReasonPolicyRejected + AutoTierSwitchFailureReasonRequestFailed = rpc.AutoTierSwitchFailureReasonRequestFailed + AutoTierSwitchFailureReasonSetupFailed = rpc.AutoTierSwitchFailureReasonSetupFailed + AutoTierSwitchFailureReasonUnsupported = rpc.AutoTierSwitchFailureReasonUnsupported BinaryAssetReferenceTypeImage = rpc.BinaryAssetReferenceTypeImage BinaryAssetReferenceTypeResource = rpc.BinaryAssetReferenceTypeResource BinaryAssetTypeImage = rpc.BinaryAssetTypeImage @@ -750,6 +758,7 @@ const ( SessionEventTypeSandboxDecision = rpc.SessionEventTypeSandboxDecision SessionEventTypeSessionAutoModeResolved = rpc.SessionEventTypeSessionAutoModeResolved SessionEventTypeSessionAutopilotObjectiveChanged = rpc.SessionEventTypeSessionAutopilotObjectiveChanged + SessionEventTypeSessionAutoTierSwitchFailed = rpc.SessionEventTypeSessionAutoTierSwitchFailed SessionEventTypeSessionBackgroundTasksChanged = rpc.SessionEventTypeSessionBackgroundTasksChanged SessionEventTypeSessionBinaryAsset = rpc.SessionEventTypeSessionBinaryAsset SessionEventTypeSessionCanvasClosed = rpc.SessionEventTypeSessionCanvasClosed @@ -779,6 +788,8 @@ const ( SessionEventTypeSessionLimitsExhaustedRequested = rpc.SessionEventTypeSessionLimitsExhaustedRequested SessionEventTypeSessionManagedSettingsEnforced = rpc.SessionEventTypeSessionManagedSettingsEnforced SessionEventTypeSessionManagedSettingsResolved = rpc.SessionEventTypeSessionManagedSettingsResolved + SessionEventTypeSessionMCPServerNeedsReconnect = rpc.SessionEventTypeSessionMCPServerNeedsReconnect + SessionEventTypeSessionMCPServerRemoved = rpc.SessionEventTypeSessionMCPServerRemoved SessionEventTypeSessionMCPServersLoaded = rpc.SessionEventTypeSessionMCPServersLoaded SessionEventTypeSessionMCPServerStatusChanged = rpc.SessionEventTypeSessionMCPServerStatusChanged SessionEventTypeSessionModeChanged = rpc.SessionEventTypeSessionModeChanged diff --git a/java/pom.xml b/java/pom.xml index 285cb7bb9a..bf21d05af4 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -63,7 +63,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.83-3 + ^1.0.83-4 true diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 44a6d9fe9d..a394c2717f 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.83-3", + "@github/copilot": "^1.0.83-4", "json-schema": "^0.4.0", "tsx": "^4.23.13" } @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-3", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-3.tgz", - "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==", + "version": "1.0.83-4", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-4.tgz", + "integrity": "sha512-IiUDou0khxU8hu3Xjfq0uwp08qqZwJROizROC+ZMUZaUZGOMD64UXi4N/hauosTEnL/kI/WxzYKw4kO7W6LtpQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-3", - "@github/copilot-darwin-x64": "1.0.83-3", - "@github/copilot-linux-arm64": "1.0.83-3", - "@github/copilot-linux-x64": "1.0.83-3", - "@github/copilot-linuxmusl-arm64": "1.0.83-3", - "@github/copilot-linuxmusl-x64": "1.0.83-3", - "@github/copilot-win32-arm64": "1.0.83-3", - "@github/copilot-win32-x64": "1.0.83-3" + "@github/copilot-darwin-arm64": "1.0.83-4", + "@github/copilot-darwin-x64": "1.0.83-4", + "@github/copilot-linux-arm64": "1.0.83-4", + "@github/copilot-linux-x64": "1.0.83-4", + "@github/copilot-linuxmusl-arm64": "1.0.83-4", + "@github/copilot-linuxmusl-x64": "1.0.83-4", + "@github/copilot-win32-arm64": "1.0.83-4", + "@github/copilot-win32-x64": "1.0.83-4" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-3", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-3.tgz", - "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==", + "version": "1.0.83-4", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-4.tgz", + "integrity": "sha512-rImrvW6dGC16Fu2MLPGNyxb1Nav9n+E5o4hKqyzC1NvK+9kknH8vPTosDEJt4+QEYPCGqyicTjfEoFvQjam6gg==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-3", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-3.tgz", - "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==", + "version": "1.0.83-4", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-4.tgz", + "integrity": "sha512-w7ZGwEwDutVLBfkO0WcJaHuNpv/OZxHbtj0cAzGFHYt6NiC7Pk/VQ9X+OAZBlVby/75yCC0Lf2SlIHM7/4Weqw==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-3", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-3.tgz", - "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==", + "version": "1.0.83-4", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-4.tgz", + "integrity": "sha512-UksdtQRk+sYVNP+X0VhGDhmmOE1kW9Bmnkj+1zLspwRxuzAZwLY5LXzrZ0/HIw5s6xQKDTWJKGWPlRvipK3mrQ==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-3", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-3.tgz", - "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==", + "version": "1.0.83-4", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-4.tgz", + "integrity": "sha512-Pxc6f9ear0vt0oMkG1tMxxDKQBsazaUiU7fcWYlfA1qBog0Q+UYoFxAmENNVPREPZ71nwM7P6gW7iO/GdMfYug==", "cpu": [ "x64" ], @@ -513,26 +513,10 @@ "copilot-linux-x64": "copilot" } }, - "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-3", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-3.tgz", - "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-arm64": "copilot" - } - }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-3", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-3.tgz", - "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==", + "version": "1.0.83-4", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-4.tgz", + "integrity": "sha512-Gg6amLZ6M2kxVSCghLcFpT+T/mwIJLK/oV5XyHZW0wnsYaoaeTlAUpJJ+K4JwRxyxTeFGxRV7woUmY3iWRU1EQ==", "cpu": [ "x64" ], @@ -546,9 +530,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-3", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-3.tgz", - "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==", + "version": "1.0.83-4", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-4.tgz", + "integrity": "sha512-ZYqGCzJyQlw4DquuIJQAKEud9OpNizOgpTwoLTgf6ej3glqM+ISu8iVIQ9EQFhaI6m9KrXJ4iwCpFjUwv7zWDQ==", "cpu": [ "arm64" ], @@ -561,22 +545,6 @@ "copilot-win32-arm64": "copilot.exe" } }, - "node_modules/@github/copilot-win32-x64": { - "version": "1.0.83-3", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-3.tgz", - "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-x64": "copilot.exe" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index a9f84732a2..4010551edd 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.83-3", + "@github/copilot": "^1.0.83-4", "json-schema": "^0.4.0", "tsx": "^4.23.13" } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AutoTierSwitchFailureReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/AutoTierSwitchFailureReason.java new file mode 100644 index 0000000000..4180045ace --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AutoTierSwitchFailureReason.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; + +/** + * Terminal reason an Auto preference activation failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutoTierSwitchFailureReason { + /** The {@code policy_rejected} variant. */ + POLICY_REJECTED("policy_rejected"), + /** The {@code request_failed} variant. */ + REQUEST_FAILED("request_failed"), + /** The {@code setup_failed} variant. */ + SETUP_FAILED("setup_failed"), + /** The {@code unsupported} variant. */ + UNSUPPORTED("unsupported"); + + private final String value; + AutoTierSwitchFailureReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AutoTierSwitchFailureReason fromValue(String value) { + for (AutoTierSwitchFailureReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutoTierSwitchFailureReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoTierSwitchFailedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoTierSwitchFailedEvent.java new file mode 100644 index 0000000000..7509bb678f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoTierSwitchFailedEvent.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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 "session.auto_tier_switch_failed". A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionAutoTierSwitchFailedEvent extends SessionEvent { + + @Override + public String getType() { return "session.auto_tier_switch_failed"; } + + @JsonProperty("data") + private SessionAutoTierSwitchFailedEventData data; + + public SessionAutoTierSwitchFailedEventData getData() { return data; } + public void setData(SessionAutoTierSwitchFailedEventData data) { this.data = data; } + + /** Data payload for {@link SessionAutoTierSwitchFailedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionAutoTierSwitchFailedEventData( + /** Auto preference that remains effective after the failed request. */ + @JsonProperty("effectiveAutoTier") AutoTier effectiveAutoTier, + /** Auto preference that failed to activate, or null when returning to provider-default routing failed. */ + @JsonProperty("requestedAutoTier") AutoTier requestedAutoTier, + /** Low-cardinality failure outcome reported by Auto resolution. */ + @JsonProperty("reason") AutoTierSwitchFailureReason reason + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java index 193aa56b8f..367fa120b5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -38,6 +38,7 @@ @JsonSubTypes.Type(value = SessionInfoEvent.class, name = "session.info"), @JsonSubTypes.Type(value = SessionWarningEvent.class, name = "session.warning"), @JsonSubTypes.Type(value = SessionModelChangeEvent.class, name = "session.model_change"), + @JsonSubTypes.Type(value = SessionAutoTierSwitchFailedEvent.class, name = "session.auto_tier_switch_failed"), @JsonSubTypes.Type(value = SessionModeChangedEvent.class, name = "session.mode_changed"), @JsonSubTypes.Type(value = SessionModeNoticeDeliveredEvent.class, name = "session.mode_notice_delivered"), @JsonSubTypes.Type(value = SessionSessionLimitsChangedEvent.class, name = "session.session_limits_changed"), @@ -146,6 +147,8 @@ @JsonSubTypes.Type(value = SessionCustomAgentsUpdatedEvent.class, name = "session.custom_agents_updated"), @JsonSubTypes.Type(value = SessionMcpServersLoadedEvent.class, name = "session.mcp_servers_loaded"), @JsonSubTypes.Type(value = SessionMcpServerStatusChangedEvent.class, name = "session.mcp_server_status_changed"), + @JsonSubTypes.Type(value = SessionMcpServerRemovedEvent.class, name = "session.mcp_server_removed"), + @JsonSubTypes.Type(value = SessionMcpServerNeedsReconnectEvent.class, name = "session.mcp_server_needs_reconnect"), @JsonSubTypes.Type(value = McpToolsListChangedEvent.class, name = "mcp.tools.list_changed"), @JsonSubTypes.Type(value = McpResourcesListChangedEvent.class, name = "mcp.resources.list_changed"), @JsonSubTypes.Type(value = McpPromptsListChangedEvent.class, name = "mcp.prompts.list_changed"), @@ -174,6 +177,7 @@ public abstract sealed class SessionEvent permits SessionInfoEvent, SessionWarningEvent, SessionModelChangeEvent, + SessionAutoTierSwitchFailedEvent, SessionModeChangedEvent, SessionModeNoticeDeliveredEvent, SessionSessionLimitsChangedEvent, @@ -282,6 +286,8 @@ public abstract sealed class SessionEvent permits SessionCustomAgentsUpdatedEvent, SessionMcpServersLoadedEvent, SessionMcpServerStatusChangedEvent, + SessionMcpServerRemovedEvent, + SessionMcpServerNeedsReconnectEvent, McpToolsListChangedEvent, McpResourcesListChangedEvent, McpPromptsListChangedEvent, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerNeedsReconnectEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerNeedsReconnectEvent.java new file mode 100644 index 0000000000..9b03a66b94 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerNeedsReconnectEvent.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 "session.mcp_server_needs_reconnect". Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpServerNeedsReconnectEvent extends SessionEvent { + + @Override + public String getType() { return "session.mcp_server_needs_reconnect"; } + + @JsonProperty("data") + private SessionMcpServerNeedsReconnectEventData data; + + public SessionMcpServerNeedsReconnectEventData getData() { return data; } + public void setData(SessionMcpServerNeedsReconnectEventData data) { this.data = data; } + + /** Data payload for {@link SessionMcpServerNeedsReconnectEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionMcpServerNeedsReconnectEventData( + /** Name of the MCP server that needs to reconnect */ + @JsonProperty("serverName") String serverName + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerRemovedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerRemovedEvent.java new file mode 100644 index 0000000000..68b5107118 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerRemovedEvent.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 "session.mcp_server_removed". Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpServerRemovedEvent extends SessionEvent { + + @Override + public String getType() { return "session.mcp_server_removed"; } + + @JsonProperty("data") + private SessionMcpServerRemovedEventData data; + + public SessionMcpServerRemovedEventData getData() { return data; } + public void setData(SessionMcpServerRemovedEventData data) { this.data = data; } + + /** Data payload for {@link SessionMcpServerRemovedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionMcpServerRemovedEventData( + /** Name of the MCP server that was removed from the graph */ + @JsonProperty("serverName") String serverName + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java index 868d78fe0d..4ea1dbd46d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java @@ -55,7 +55,11 @@ public record SessionModelChangeEventData( /** Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. */ @JsonProperty("cause") String cause, /** Origin of the effective model change, when known. */ - @JsonProperty("source") ModelChangeSource source + @JsonProperty("source") ModelChangeSource source, + /** Previously committed Auto preference, when one was explicitly selected. */ + @JsonProperty("previousAutoTier") AutoTier previousAutoTier, + /** Committed Auto preference after the model configuration change, when applicable. */ + @JsonProperty("autoTier") AutoTier autoTier ) { } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java index e77117b2cb..4fd7a91ca2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record CapiSessionOptions( - /** Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. */ + /** Routing preference for sessions whose model is `auto`. On create or cold resume, this establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold resume, the runtime restores the last committed preference. On resident resume, a different value requests a safe switch after resume succeeds and cannot change an in-flight turn. Successful switches are persisted for later cold resume. When no preference is supplied or restored, CAPI default routing is used. */ @JsonProperty("autoTier") AutoTier autoTier, /** Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. */ @JsonProperty("enableWebSocketResponses") Boolean enableWebSocketResponses diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ClientTaskCancelReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ClientTaskCancelReason.java new file mode 100644 index 0000000000..723e7e80b0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ClientTaskCancelReason.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Why the runtime requests client-task cancellation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ClientTaskCancelReason { + /** The {@code cancel_requested} variant. */ + CANCEL_REQUESTED("cancel_requested"), + /** The {@code session_shutdown} variant. */ + SESSION_SHUTDOWN("session_shutdown"); + + private final String value; + ClientTaskCancelReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ClientTaskCancelReason fromValue(String value) { + for (ClientTaskCancelReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ClientTaskCancelReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java index 05f2534970..0975b7bd87 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import java.util.List; import javax.annotation.processing.Generated; /** @@ -28,6 +29,8 @@ public record ConnectParams( @JsonProperty("enableGitHubTelemetryForwarding") Boolean enableGitHubTelemetryForwarding, /** Identity of the integrating host. Optional; omit it to keep the default attribution. */ @JsonProperty("clientInfo") ConnectClientInfo clientInfo, + /** Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. */ + @JsonProperty("supportedTaskKinds") List supportedTaskKinds, /** Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN */ @JsonProperty("token") String token ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java index 8c12b57a80..41a200ff4b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import java.util.List; import javax.annotation.processing.Generated; /** @@ -29,6 +30,8 @@ public record ConnectResult( /** Server protocol version number */ @JsonProperty("protocolVersion") Long protocolVersion, /** Server package version */ - @JsonProperty("version") String version + @JsonProperty("version") String version, + /** Task kinds the server may return to this connection. */ + @JsonProperty("taskKinds") List taskKinds ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java new file mode 100644 index 0000000000..bf5f9a8b97 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CurrentModel( + /** Currently active model identifier */ + @JsonProperty("modelId") String modelId, + /** Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ + @JsonProperty("reasoningEffort") String reasoningEffort, + /** Context tier for models that support multiple context-window sizes. */ + @JsonProperty("contextTier") ContextTier contextTier, + /** Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. */ + @JsonProperty("autoTier") AutoTier autoTier, + /** Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. */ + @JsonProperty("pendingAutoTier") AutoTier pendingAutoTier, + /** Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. */ + @JsonProperty("activatingAutoTier") AutoTier activatingAutoTier +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigRemoveParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigRemoveParams.java index 81a0aa0e21..74258dcfc5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigRemoveParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigRemoveParams.java @@ -25,6 +25,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record McpConfigRemoveParams( /** Name of the MCP server to remove */ - @JsonProperty("name") String name + @JsonProperty("name") String name, + /** OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. */ + @JsonProperty("authClientIdMetadataUrl") String authClientIdMetadataUrl ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java index a652e8f4f6..3e9f0b6b87 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +import java.util.Map; import javax.annotation.processing.Generated; /** @@ -28,6 +29,8 @@ public record Model( @JsonProperty("name") String name, /** Model capabilities and limits */ @JsonProperty("capabilities") ModelCapabilities capabilities, + /** Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. */ + @JsonProperty("metadata") Map metadata, /** Policy state (if applicable) */ @JsonProperty("policy") ModelPolicy policy, /** Billing information */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java new file mode 100644 index 0000000000..e4a1baed5e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Whether the requested preference was already effective or was accepted for later transactional activation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelSwitchAutoTierStatus { + /** The {@code unchanged} variant. */ + UNCHANGED("unchanged"), + /** The {@code pending} variant. */ + PENDING("pending"); + + private final String value; + ModelSwitchAutoTierStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelSwitchAutoTierStatus fromValue(String value) { + for (ModelSwitchAutoTierStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelSwitchAutoTierStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java index beda6b20a2..dd522f7a34 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java @@ -27,6 +27,16 @@ public record SandboxConfig( @JsonProperty("userPolicy") SandboxConfigUserPolicy userPolicy, /** Whether to auto-add the current working directory to readwritePaths. Default: true. */ @JsonProperty("addCurrentWorkingDirectory") Boolean addCurrentWorkingDirectory, + /** Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). */ + @JsonProperty("sandboxMcpServers") Boolean sandboxMcpServers, + /** Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). */ + @JsonProperty("sandboxLspServers") Boolean sandboxLspServers, + /** Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). */ + @JsonProperty("allowBypass") Boolean allowBypass, + /** Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. */ + @JsonProperty("managedMcpRoutingLocked") Boolean managedMcpRoutingLocked, + /** The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. */ + @JsonProperty("managedLspRoutingLocked") Boolean managedLspRoutingLocked, /** Credential-injection capability flags. */ @JsonProperty("auth") SandboxConfigAuth auth, /** Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java index e85b7b987a..e6013c870d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java @@ -37,4 +37,15 @@ public CompletableFuture read() { return caller.invoke("managedSettings.read", java.util.Map.of(), ManagedSettingsReadResult.class); } + /** + * Invokes {@code managedSettings.clearCache}. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture clearCache() { + return caller.invoke("managedSettings.clearCache", java.util.Map.of(), Void.class); + } + } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java index 12777c9a1f..b9adc34484 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java @@ -57,6 +57,22 @@ public CompletableFuture switchTo(SessionModelSwitch return caller.invoke("session.model.switchTo", _p, SessionModelSwitchToResult.class); } + /** + * An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + *

+ * 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 switchAutoTier(SessionModelSwitchAutoTierParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.model.switchAutoTier", _p, SessionModelSwitchAutoTierResult.class); + } + /** * Managed, repository, and CLI model overrides to overlay onto the session at startup. *

diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java index f29e249c32..53ce443e08 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java @@ -40,6 +40,8 @@ public record SessionModelApplyStartupOverlayResult( /** User-facing warning produced while applying the model switch. */ @JsonProperty("warning") String warning, /** Deprecation warnings associated with the selected model or options. */ - @JsonProperty("deprecationWarnings") List deprecationWarnings + @JsonProperty("deprecationWarnings") List deprecationWarnings, + /** Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. */ + @JsonProperty("modelState") CurrentModel modelState ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java index 21afab2fa4..23a9788540 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + * The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -29,6 +29,12 @@ public record SessionModelGetCurrentResult( /** Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Context tier for models that support multiple context-window sizes. */ - @JsonProperty("contextTier") ContextTier contextTier + @JsonProperty("contextTier") ContextTier contextTier, + /** Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. */ + @JsonProperty("autoTier") AutoTier autoTier, + /** Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. */ + @JsonProperty("pendingAutoTier") AutoTier pendingAutoTier, + /** Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. */ + @JsonProperty("activatingAutoTier") AutoTier activatingAutoTier ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierParams.java new file mode 100644 index 0000000000..576df55aa1 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierParams.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; + +/** + * An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + * + * @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 SessionModelSwitchAutoTierParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. */ + @JsonProperty("autoTier") AutoTier autoTier, + /** Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. */ + @JsonProperty("source") ModelChangeSource source +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierResult.java new file mode 100644 index 0000000000..7e95695492 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierResult.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + * + * @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 SessionModelSwitchAutoTierResult( + /** Immediate request status. `pending` means accepted but not committed. */ + @JsonProperty("status") ModelSwitchAutoTierStatus status, + /** Auto preference currently committed for the session. */ + @JsonProperty("effectiveAutoTier") AutoTier effectiveAutoTier, + /** Latest unclaimed Auto preference waiting for a future user turn. */ + @JsonProperty("pendingAutoTier") AutoTier pendingAutoTier, + /** Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. */ + @JsonProperty("activatingAutoTier") AutoTier activatingAutoTier, + /** Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. */ + @JsonProperty("supersededAutoTier") AutoTier supersededAutoTier +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java index d7e36cf3c9..cfd59bcf36 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java @@ -28,6 +28,8 @@ public record SessionModelSwitchToParams( @JsonProperty("sessionId") String sessionId, /** Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. */ @JsonProperty("modelId") String modelId, + /** Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. */ + @JsonProperty("autoTier") AutoTier autoTier, /** Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. When omitted, no effort override is applied. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Reasoning summary mode to request for supported model clients */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToResult.java index fb143abc30..47099e42ea 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToResult.java @@ -40,6 +40,8 @@ public record SessionModelSwitchToResult( /** User-facing warning produced while applying the model switch. */ @JsonProperty("warning") String warning, /** Deprecation warnings associated with the selected model or options. */ - @JsonProperty("deprecationWarnings") List deprecationWarnings + @JsonProperty("deprecationWarnings") List deprecationWarnings, + /** Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. */ + @JsonProperty("modelState") CurrentModel modelState ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java index 5f4753b1ac..a1c234903e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java @@ -37,6 +37,8 @@ public record SessionOpenOptions( @JsonProperty("verbosity") Verbosity verbosity, /** Identifier of the client driving the session. */ @JsonProperty("clientName") String clientName, + /** OAuth Client ID Metadata Document URL used by this host for MCP authorization. */ + @JsonProperty("authClientIdMetadataUrl") String authClientIdMetadataUrl, /** Structured client kind used for runtime behavior gates. */ @JsonProperty("clientKind") String clientKind, /** Identifier sent to LSP-style integrations. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java index 68f038eb4b..c6ca1335de 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java @@ -57,6 +57,38 @@ public CompletableFuture list() { return caller.invoke("session.tasks.list", java.util.Map.of("sessionId", this.sessionId), SessionTasksListResult.class); } + /** + * Registers or reclaims a client-owned task. + *

+ * 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 register(SessionTasksRegisterParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.register", _p, SessionTasksRegisterResult.class); + } + + /** + * Updates a client-owned task. + *

+ * 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 update(SessionTasksUpdateParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.update", _p, SessionTasksUpdateResult.class); + } + /** * Identifies the target session. * diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterParams.java new file mode 100644 index 0000000000..0b14af08fd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterParams.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Registers or reclaims a client-owned task. + * + * @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 SessionTasksRegisterParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task kind */ + @JsonProperty("type") TaskClientType type, + /** Owner-scoped idempotency key used for registration and reclaim */ + @JsonProperty("clientTaskId") String clientTaskId, + /** Human-readable description of the external work */ + @JsonProperty("description") String description, + /** Optional short display name for the external work */ + @JsonProperty("displayName") String displayName, + /** Whether the owner supports runtime cancellation requests */ + @JsonProperty("cancellable") Boolean cancellable, + /** Expected current sequence for idempotent registration or orphan reclaim */ + @JsonProperty("expectedSequence") Long expectedSequence +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterResult.java new file mode 100644 index 0000000000..f7e25fe885 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterResult.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; + +/** + * Result of registering or reclaiming a client-owned task. + * + * @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 SessionTasksRegisterResult( + /** Authoritative registered or reclaimed task */ + @JsonProperty("task") TaskClientInfo task, + /** True only when this invocation created a new task */ + @JsonProperty("created") Boolean created, + /** True only when this invocation reclaimed an orphaned task */ + @JsonProperty("reclaimed") Boolean reclaimed +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateParams.java new file mode 100644 index 0000000000..ecd06a1b7e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateParams.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Updates a client-owned task. + * + * @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 SessionTasksUpdateParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Canonical runtime-generated task identifier */ + @JsonProperty("id") String id, + /** Owner update sequence to apply */ + @JsonProperty("sequence") Long sequence, + /** Progress or terminal update payload */ + @JsonProperty("update") Object update +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateResult.java new file mode 100644 index 0000000000..8a08e87859 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateResult.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; + +/** + * Result of publishing a client-owned task update. + * + * @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 SessionTasksUpdateResult( + /** Authoritative task after processing the update */ + @JsonProperty("task") TaskClientInfo task, + /** Whether this invocation changed task state */ + @JsonProperty("applied") Boolean applied, + /** Whether this invocation repeated the latest accepted update */ + @JsonProperty("duplicate") Boolean duplicate +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientExecutionMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientExecutionMode.java new file mode 100644 index 0000000000..6c948f86e0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientExecutionMode.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Client-owned tasks always execute outside the runtime in background mode. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientExecutionMode { + /** The {@code background} variant. */ + BACKGROUND("background"); + + private final String value; + TaskClientExecutionMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientExecutionMode fromValue(String value) { + for (TaskClientExecutionMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientExecutionMode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientInfo.java new file mode 100644 index 0000000000..6a221303eb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientInfo.java @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Tracked client-owned task metadata. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record TaskClientInfo( + /** Task kind */ + @JsonProperty("type") TaskClientType type, + /** Canonical runtime-generated task identifier */ + @JsonProperty("id") String id, + /** Owner-scoped registration and reclaim key */ + @JsonProperty("clientTaskId") String clientTaskId, + /** Optional task display name */ + @JsonProperty("displayName") String displayName, + /** Task description */ + @JsonProperty("description") String description, + /** Client task lifecycle status */ + @JsonProperty("status") TaskClientStatus status, + /** Public attribution and presence for the task owner */ + @JsonProperty("owner") TaskClientOwner owner, + /** ISO 8601 timestamp when the task started */ + @JsonProperty("startedAt") OffsetDateTime startedAt, + /** ISO 8601 timestamp of the latest accepted lifecycle change */ + @JsonProperty("updatedAt") OffsetDateTime updatedAt, + /** ISO 8601 timestamp when the task reached a terminal status */ + @JsonProperty("completedAt") OffsetDateTime completedAt, + /** Accumulated active execution time in milliseconds */ + @JsonProperty("activeTimeMs") Long activeTimeMs, + /** ISO 8601 timestamp when the current active segment started */ + @JsonProperty("activeStartedAt") OffsetDateTime activeStartedAt, + /** ISO 8601 timestamp when the connected owner entered idle status */ + @JsonProperty("idleSince") OffsetDateTime idleSince, + /** ISO 8601 timestamp of the most recent orphan transition */ + @JsonProperty("orphanedAt") OffsetDateTime orphanedAt, + /** ISO 8601 timestamp of the most recent successful reclaim */ + @JsonProperty("reclaimedAt") OffsetDateTime reclaimedAt, + /** Execution mode, which is always background for client-owned tasks */ + @JsonProperty("executionMode") TaskClientExecutionMode executionMode, + /** Whether the currently bound owner can receive a cancellation request */ + @JsonProperty("canCancel") Boolean canCancel, + /** Sequence number of the latest accepted owner update */ + @JsonProperty("sequence") Long sequence, + /** Opaque successful terminal result supplied by the task owner */ + @JsonProperty("result") Object result, + /** Human-readable terminal failure message */ + @JsonProperty("error") String error, + /** Optional owner-supplied terminal failure code */ + @JsonProperty("errorCode") String errorCode, + /** Human-readable reason for terminal cancellation */ + @JsonProperty("cancellationReason") String cancellationReason +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwner.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwner.java new file mode 100644 index 0000000000..e2ce54019b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwner.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record TaskClientOwner( + /** Opaque session-scoped participant identity */ + @JsonProperty("participantId") String participantId, + /** Opaque identity of the currently or most recently bound session join */ + @JsonProperty("joinId") String joinId, + /** Class of the task owner */ + @JsonProperty("kind") TaskClientOwnerKind kind, + /** Display-only owner name */ + @JsonProperty("displayName") String displayName, + /** Display-only owner source */ + @JsonProperty("source") String source, + /** Whether this task's bound join is currently connected */ + @JsonProperty("presence") TaskClientOwnerPresence presence, + /** ISO 8601 timestamp when the bound join disconnected */ + @JsonProperty("disconnectedAt") OffsetDateTime disconnectedAt +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerKind.java new file mode 100644 index 0000000000..92518264a5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerKind.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Connection class owning a client task. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientOwnerKind { + /** The {@code extension} variant. */ + EXTENSION("extension"), + /** The {@code sdk} variant. */ + SDK("sdk"); + + private final String value; + TaskClientOwnerKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientOwnerKind fromValue(String value) { + for (TaskClientOwnerKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientOwnerKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerPresence.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerPresence.java new file mode 100644 index 0000000000..282cc69206 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerPresence.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Presence of the task's bound join. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientOwnerPresence { + /** The {@code connected} variant. */ + CONNECTED("connected"), + /** The {@code disconnected} variant. */ + DISCONNECTED("disconnected"); + + private final String value; + TaskClientOwnerPresence(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientOwnerPresence fromValue(String value) { + for (TaskClientOwnerPresence v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientOwnerPresence value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientStatus.java new file mode 100644 index 0000000000..a72daddc41 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientStatus.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Lifecycle status of a client-owned task. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientStatus { + /** The {@code running} variant. */ + RUNNING("running"), + /** The {@code idle} variant. */ + IDLE("idle"), + /** The {@code completed} variant. */ + COMPLETED("completed"), + /** The {@code failed} variant. */ + FAILED("failed"), + /** The {@code cancelled} variant. */ + CANCELLED("cancelled"), + /** The {@code orphaned} variant. */ + ORPHANED("orphaned"); + + private final String value; + TaskClientStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientStatus fromValue(String value) { + for (TaskClientStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientType.java new file mode 100644 index 0000000000..42f74cf3dd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientType.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Discriminator for a client-owned task. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientType { + /** The {@code client} variant. */ + CLIENT("client"); + + private final String value; + TaskClientType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientType fromValue(String value) { + for (TaskClientType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientType value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java new file mode 100644 index 0000000000..413859db20 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Closed set of public task kinds a connection can negotiate. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskKind { + /** The {@code agent} variant. */ + AGENT("agent"), + /** The {@code shell} variant. */ + SHELL("shell"), + /** The {@code client} variant. */ + CLIENT("client"); + + private final String value; + TaskKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskKind fromValue(String value) { + for (TaskKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelParams.java new file mode 100644 index 0000000000..57cbd5e6e0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelParams.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Runtime-to-owner cancellation request for a client-owned task. + * + * @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 TasksCancelParams( + /** Session that owns the client task */ + @JsonProperty("sessionId") String sessionId, + /** Canonical runtime-generated task identifier */ + @JsonProperty("id") String id, + /** Owner-scoped task key included for correlation */ + @JsonProperty("clientTaskId") String clientTaskId, + /** Opaque identifier shared by coalesced cancellation callers */ + @JsonProperty("cancellationId") String cancellationId, + /** Reason the runtime requests cancellation */ + @JsonProperty("reason") ClientTaskCancelReason reason +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelResult.java new file mode 100644 index 0000000000..bd549d1df4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelResult.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; + +/** + * Whether the client authoritatively confirmed its external work stopped. + * + * @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 TasksCancelResult( + /** True only when the owner confirms that external work stopped before responding */ + @JsonProperty("cancelled") Boolean cancelled +) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index 56c3487635..fbd4728739 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -638,6 +638,7 @@ private void verifyProtocolVersion(Connection connection) throws Exception { if (effectiveConnectionToken != null) { connectParams.put("token", effectiveConnectionToken); } + connectParams.put("supportedTaskKinds", List.of("agent", "client", "shell")); // Opt into GitHub telemetry forwarding at the connection level when a handler // is registered, so the runtime can forward the first session's un-replayable // start event. Also sent on session create/resume for backward compatibility diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index f3a35967d3..c53fea3f4b 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -2010,8 +2010,8 @@ public CompletableFuture abort() { */ public CompletableFuture setModel(String model, String reasoningEffort) { ensureNotTerminated(); - return getRpc().model.switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, null, null, - null, null, null, null, null, null, null, null, null, null)).thenApply(r -> null); + return getRpc().model.switchTo(new SessionModelSwitchToParams(sessionId, model, null, reasoningEffort, null, + null, null, null, null, null, null, null, null, null, null, null)).thenApply(r -> null); } /** @@ -2091,9 +2091,9 @@ public CompletableFuture setModel(String model, String reasoningEffort, St var generatedReasoningSummary = reasoningSummary == null ? null : com.github.copilot.generated.rpc.ReasoningSummary.fromValue(reasoningSummary); - return getRpc().model - .switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, generatedReasoningSummary, - null, generatedCapabilities, null, null, null, null, null, null, null, null, null)) + return getRpc().model.switchTo( + new SessionModelSwitchToParams(sessionId, model, null, reasoningEffort, generatedReasoningSummary, null, + generatedCapabilities, null, null, null, null, null, null, null, null, null)) .thenApply(r -> null); } diff --git a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java index 5a6e378b2a..e344b4f748 100644 --- a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java +++ b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java @@ -189,6 +189,7 @@ void clientOmitsForwardingWhenNoHandler() throws Exception { JsonNode connectParams = server.awaitConnect(); assertFalse(connectParams.has("enableGitHubTelemetryForwarding"), "connect request should omit the flag when no handler is registered"); + assertEquals("[\"agent\",\"client\",\"shell\"]", connectParams.path("supportedTaskKinds").toString()); client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(15, TimeUnit.SECONDS); diff --git a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java index f7fc58d429..235b1d5720 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java @@ -65,7 +65,7 @@ void testShouldAddByokProviderAndModelAtRuntime() throws Exception { var selectionId = "java-e2e-provider/small"; session.getRpc().model.switchTo(new SessionModelSwitchToParams(null, selectionId, null, null, null, - null, null, null, null, null, null, null, null, null, null)).get(30, TimeUnit.SECONDS); + null, null, null, null, null, null, null, null, null, null, null)).get(30, TimeUnit.SECONDS); var current = session.getRpc().model.getCurrent().get(30, TimeUnit.SECONDS); assertEquals(selectionId, current.modelId()); } diff --git a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java index 3bb9001f35..7b55748bb8 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java @@ -206,7 +206,7 @@ void sessionRpc_model_switchTo_merges_sessionId_with_extra_params() { // switchTo takes extra params beyond sessionId var switchParams = new SessionModelSwitchToParams(null, "gpt-5", null, null, null, null, null, null, null, null, - null, null, null, null, null); + null, null, null, null, null, null); session.model.switchTo(switchParams); assertEquals(1, stub.calls.size()); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java index fd0fdaaea1..191c290026 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java @@ -97,7 +97,7 @@ void serverRpc_mcp_config_remove_invokes_correct_method() { var stub = new StubCaller(); var server = new ServerRpc(stub); - var params = new McpConfigRemoveParams("myServer"); + var params = new McpConfigRemoveParams("myServer", null); server.mcp.config.remove(params); assertEquals(1, stub.calls.size()); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index f86b3dfbcb..1702561bdc 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -48,7 +48,7 @@ void mcpDiscoverParams_record() { @Test void mcpConfigRemoveParams_record() { - var params = new McpConfigRemoveParams("old-server"); + var params = new McpConfigRemoveParams("old-server", null); assertEquals("old-server", params.name()); } @@ -326,8 +326,8 @@ void sessionModelGetCurrentParams_record() { @Test void sessionModelSwitchToParams_record() { - var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-5", "high", null, null, null, null, null, - null, null, null, null, null, null, null); + var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-5", null, "high", null, null, null, null, + null, null, null, null, null, null, null, null); assertEquals("sess-32", params.sessionId()); assertEquals("claude-sonnet-5", params.modelId()); assertEquals("high", params.reasoningEffort()); @@ -657,13 +657,13 @@ void sessionMcpListResult_status_enum_all_values() { @Test void sessionModelGetCurrentResult_record() { - var result = new SessionModelGetCurrentResult("claude-sonnet-5", null, null); + var result = new SessionModelGetCurrentResult("claude-sonnet-5", null, null, null, null, null); assertEquals("claude-sonnet-5", result.modelId()); } @Test void sessionModelSwitchToResult_record() { - var result = new SessionModelSwitchToResult("gpt-5", true, null, null, null, null, null, null); + var result = new SessionModelSwitchToResult("gpt-5", true, null, null, null, null, null, null, null); assertEquals("gpt-5", result.modelId()); assertEquals(true, result.deferred()); } @@ -819,8 +819,8 @@ void modelsListResult_nested() { var policy = new ModelPolicy(ModelPolicyState.ENABLED, null); var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount", true); var billing = new ModelBilling(1.0, null, null, promo); - var modelItem = new Model("gpt-5", "GPT-5", capabilities, policy, billing, null, null, null, null, null, null, - null, null); + var modelItem = new Model("gpt-5", "GPT-5", capabilities, null, policy, billing, null, null, null, null, null, + null, null, null); var result = new ModelsListResult(List.of(modelItem)); assertEquals(1, result.models().size()); @@ -857,8 +857,8 @@ void sessionModelSwitchToParams_nested_records() { var limits = new ModelCapabilitiesOverrideLimits(100000L, 8192L, 128000L, limitsVision); var supports = new ModelCapabilitiesOverrideSupports(true, true, null); var capabilities = new ModelCapabilitiesOverride(supports, limits); - var params = new SessionModelSwitchToParams("sess-m", "gpt-5", null, null, null, capabilities, null, null, null, - null, null, null, null, null, null); + var params = new SessionModelSwitchToParams("sess-m", "gpt-5", null, null, null, null, capabilities, null, null, + null, null, null, null, null, null, null); assertEquals("gpt-5", params.modelId()); assertNotNull(params.modelCapabilities()); diff --git a/nodejs/package.json b/nodejs/package.json index c937d83bf7..f64c05897e 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/github/copilot-sdk.git" }, "version": "0.0.0-dev", - "copilotCliVersion": "1.0.83-3", + "copilotCliVersion": "1.0.83-4", "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", "main": "./dist/cjs/index.js", "types": "./dist/index.d.ts", diff --git a/nodejs/src/cliVersion.ts b/nodejs/src/cliVersion.ts index a0c4eb8558..c4ad7b3045 100644 --- a/nodejs/src/cliVersion.ts +++ b/nodejs/src/cliVersion.ts @@ -1,3 +1,3 @@ -export const COPILOT_CLI_VERSION = "1.0.83-3"; +export const COPILOT_CLI_VERSION = "1.0.83-4"; export const COPILOT_CLI_USE_NPM_PACKAGE = false; diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index a942ed6776..306e1ee05b 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -38,6 +38,7 @@ import type { GitHubTokenAcquireResult, OpenCanvasInstance, SessionUpdateOptionsParams, + TaskKind, } from "./generated/rpc.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession } from "./session.js"; @@ -2177,7 +2178,11 @@ export class CopilotClient { token?: string; enableGitHubTelemetryForwarding?: boolean; clientInfo?: ConnectClientInfo; - } = { token: this.effectiveConnectionToken }; + supportedTaskKinds?: TaskKind[]; + } = { + token: this.effectiveConnectionToken, + supportedTaskKinds: ["agent", "client", "shell"], + }; // Opt in to GitHub telemetry forwarding at the connection level when a // handler is registered (mirrors the runtime, which reads this flag on the // `connect` handshake so the first session's un-replayable `session.start` diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 716f76cddc..322c13731b 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -663,6 +663,18 @@ export type CatalogUnavailableTransportReason = | "transport-not-supported" /** Eligible remotes could not be enumerated, so no explicit choice can be offered. */ | "remote-enumeration-unavailable"; +/** + * Why the runtime requests client-task cancellation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ClientTaskCancelReason". + */ +/** @experimental */ +export type ClientTaskCancelReason = + /** A caller requested task cancellation. */ + | "cancel_requested" + /** The session is shutting down. */ + | "session_shutdown"; /** * Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command * @@ -745,6 +757,20 @@ export type ConnectedRemoteSessionMetadataKind = | "remote-session" /** GitHub Copilot coding agent session. */ | "coding-agent"; +/** + * Closed set of public task kinds a connection can negotiate. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskKind". + */ +/** @experimental */ +export type TaskKind = + /** Runtime-owned background agent task. */ + | "agent" + /** Runtime-owned shell task. */ + | "shell" + /** Client-owned externally executed task. */ + | "client"; /** * Controls how MCP tool result content is filtered: none leaves content unchanged, markdown sanitizes HTML while preserving Markdown-friendly output, and hidden_characters removes characters that can hide directives. * @@ -2374,6 +2400,18 @@ export type ModelListRequest = */ skipCache?: boolean; }; +/** + * Whether the requested preference was already effective or was accepted for later transactional activation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelSwitchAutoTierStatus". + */ +/** @experimental */ +export type ModelSwitchAutoTierStatus = + /** The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. */ + | "unchanged" + /** The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. */ + | "pending"; /** * Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. * @@ -3560,13 +3598,158 @@ export type TaskExecutionMode = /** The task is managed in the background. */ | "background"; /** - * Tracked task union returned by task APIs, containing either an agent task or a shell task. + * Active status a client owner may publish with a progress update. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientActiveStatus". + */ +/** @experimental */ +export type TaskClientActiveStatus = + /** The external owner is actively working. */ + | "running" + /** The external owner is connected but waiting. */ + | "idle"; +/** + * Client-owned tasks always execute outside the runtime in background mode. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientExecutionMode". + */ +/** @experimental */ +export type TaskClientExecutionMode = "background"; +/** + * Discriminator for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientType". + */ +/** @experimental */ +export type TaskClientType = "client"; +/** + * Lifecycle status of a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientStatus". + */ +/** @experimental */ +export type TaskClientStatus = + /** The external owner is actively working. */ + | "running" + /** The external owner is connected but waiting. */ + | "idle" + /** The owner reported successful completion. */ + | "completed" + /** The owner reported failure. */ + | "failed" + /** The owner reported or confirmed cancellation. */ + | "cancelled" + /** The bound owner join disappeared; external executor state is unknown. */ + | "orphaned"; +/** + * Connection class owning a client task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientOwnerKind". + */ +/** @experimental */ +export type TaskClientOwnerKind = + /** A discovered extension connection owns the task. */ + | "extension" + /** A generic SDK connection owns the task. */ + | "sdk"; +/** + * Presence of the task's bound join. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientOwnerPresence". + */ +/** @experimental */ +export type TaskClientOwnerPresence = + /** The bound session join is connected. */ + | "connected" + /** The bound session join is disconnected. */ + | "disconnected"; +/** + * Progress or terminal update for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientUpdate". + */ +/** @experimental */ +export type TaskClientUpdate = + | { + status?: TaskClientActiveStatus; + /** + * Optional progress message appended to recent activity when nonempty + */ + message?: string; + /** + * Optional progress phase; null clears the current phase + */ + phase?: string | null; + /** + * Optional completion percentage; null clears the current percentage + */ + percentage?: number | null; + /** + * Client task update variant discriminator. + */ + kind: "progress"; + } + | { + /** + * Optional final progress message + */ + message?: string; + /** + * Optional opaque successful terminal result + */ + result?: JsonValue; + /** + * Client task update variant discriminator. + */ + kind: "completed"; + } + | { + /** + * Optional final progress message + */ + message?: string; + /** + * Human-readable terminal failure message + */ + error: string; + /** + * Optional owner-supplied terminal failure code + */ + code?: string; + /** + * Client task update variant discriminator. + */ + kind: "failed"; + } + | { + /** + * Optional final progress message + */ + message?: string; + /** + * Optional human-readable cancellation reason + */ + reason?: string; + /** + * Client task update variant discriminator. + */ + kind: "cancelled"; + }; +/** + * Tracked task union returned by task APIs, containing an agent, client, or shell task. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "TaskInfo". */ /** @experimental */ -export type TaskInfo = TaskAgentInfo | TaskShellInfo; +export type TaskInfo = TaskAgentInfo | TaskClientInfo | TaskShellInfo; /** * Whether the shell runs inside a managed PTY session or as an independent background process * @@ -3586,7 +3769,7 @@ export type TaskShellInfoAttachmentMode = * via the `definition` "TaskProgress". */ /** @experimental */ -export type TaskProgress = (TaskAgentProgress | TaskShellProgress) | null; +export type TaskProgress = TaskAgentProgress | TaskClientProgress | TaskShellProgress | null; /** * Canonical result returned by a session tool. * @@ -6047,6 +6230,45 @@ export interface CatalogUnavailableTransportError { */ message: string; } +/** + * Runtime-to-owner cancellation request for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ClientTaskCancelRequest". + */ +/** @experimental */ +export interface ClientTaskCancelRequest { + /** + * Session that owns the client task + */ + sessionId: string; + /** + * Canonical runtime-generated task identifier + */ + id: string; + /** + * Owner-scoped task key included for correlation + */ + clientTaskId: string; + /** + * Opaque identifier shared by coalesced cancellation callers + */ + cancellationId: string; + reason: ClientTaskCancelReason; +} +/** + * Whether the client authoritatively confirmed its external work stopped. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ClientTaskCancelResult". + */ +/** @experimental */ +export interface ClientTaskCancelResult { + /** + * True only when the owner confirms that external work stopped before responding + */ + cancelled: boolean; +} /** * Slash commands available in the session, after applying any include/exclude filters. * @@ -6489,6 +6711,10 @@ export interface ConnectRequest { */ enableGitHubTelemetryForwarding?: boolean; clientInfo?: ConnectClientInfo; + /** + * Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. + */ + supportedTaskKinds?: TaskKind[]; /** * Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN */ @@ -6515,6 +6741,10 @@ export interface ConnectResult { * Server package version */ version: string; + /** + * Task kinds the server may return to this connection. + */ + taskKinds?: TaskKind[]; } /** * Local file system absolute paths within the session working directory to check against its content-exclusion policy. @@ -6589,7 +6819,7 @@ export interface ContextHeaviestMessage { tokens: number; } /** - * The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + * The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "CurrentModel". @@ -6605,6 +6835,15 @@ export interface CurrentModel { */ reasoningEffort?: string; contextTier?: ContextTier; + autoTier?: AutoTier; + /** + * Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + */ + pendingAutoTier?: AutoTier | null; + /** + * Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + */ + activatingAutoTier?: AutoTier | null; } /** * Lightweight metadata for a currently initialized session tool @@ -10457,6 +10696,10 @@ export interface McpConfigRemoveRequest { * Name of the MCP server to remove */ name: string; + /** + * OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. + */ + authClientIdMetadataUrl?: string; } /** * MCP server name and replacement configuration to write to user configuration. @@ -12223,6 +12466,12 @@ export interface Model { */ name: string; capabilities: ModelCapabilities; + /** + * Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. + */ + metadata?: { + [k: string]: JsonValue | undefined; + }; policy?: ModelPolicy; billing?: ModelBilling; /** @@ -12702,6 +12951,43 @@ export interface ModelsListRequest { */ gitHubToken?: string; } +/** + * An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelSwitchAutoTierRequest". + */ +/** @experimental */ +export interface ModelSwitchAutoTierRequest { + /** + * Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. + */ + autoTier: AutoTier | null; + source?: ModelChangeSource; +} +/** + * Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelSwitchAutoTierResult". + */ +/** @experimental */ +export interface ModelSwitchAutoTierResult { + status: ModelSwitchAutoTierStatus; + effectiveAutoTier?: AutoTier; + /** + * Latest unclaimed Auto preference waiting for a future user turn. + */ + pendingAutoTier?: AutoTier | null; + /** + * Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + */ + activatingAutoTier?: AutoTier | null; + /** + * Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + */ + supersededAutoTier?: AutoTier | null; +} /** @experimental */ export interface ModelSwitchConfirmation { @@ -12730,6 +13016,10 @@ export interface ModelSwitchToRequest { * Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. */ modelId: string; + /** + * Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + */ + autoTier?: AutoTier | null; /** * Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. When omitted, no effort override is applied. */ @@ -12802,6 +13092,7 @@ export interface ModelSwitchToResult { * Deprecation warnings associated with the selected model or options. */ deprecationWarnings?: string[]; + modelState?: CurrentModel; } /** * Agent interaction mode to apply to the session. @@ -16582,6 +16873,30 @@ export interface SandboxConfig { * Whether to auto-add the current working directory to readwritePaths. Default: true. */ addCurrentWorkingDirectory?: boolean; + /** + * Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + */ + sandboxMcpServers?: boolean; + /** + * Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + */ + sandboxLspServers?: boolean; + /** + * Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). + */ + allowBypass?: boolean; + /** + * Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. + * + * @internal + */ + managedMcpRoutingLocked?: boolean; + /** + * The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + * + * @internal + */ + managedLspRoutingLocked?: boolean; auth?: SandboxConfigAuth; /** * Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). @@ -18291,6 +18606,10 @@ export interface SessionOpenOptions { * Identifier of the client driving the session. */ clientName?: string; + /** + * OAuth Client ID Metadata Document URL used by this host for MCP authorization. + */ + authClientIdMetadataUrl?: string; /** * Structured client kind used for runtime behavior gates. */ @@ -20840,6 +21159,157 @@ export interface TaskProgressLine { */ timestamp: string; } +/** + * Tracked client-owned task metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientInfo". + */ +/** @experimental */ +export interface TaskClientInfo { + type: TaskClientType; + /** + * Canonical runtime-generated task identifier + */ + id: string; + /** + * Owner-scoped registration and reclaim key + */ + clientTaskId: string; + /** + * Optional task display name + */ + displayName?: string; + /** + * Task description + */ + description: string; + status: TaskClientStatus; + owner: TaskClientOwner; + /** + * ISO 8601 timestamp when the task started + */ + startedAt: string; + /** + * ISO 8601 timestamp of the latest accepted lifecycle change + */ + updatedAt: string; + /** + * ISO 8601 timestamp when the task reached a terminal status + */ + completedAt?: string; + /** + * Accumulated active execution time in milliseconds + */ + activeTimeMs: number; + /** + * ISO 8601 timestamp when the current active segment started + */ + activeStartedAt?: string; + /** + * ISO 8601 timestamp when the connected owner entered idle status + */ + idleSince?: string; + /** + * ISO 8601 timestamp of the most recent orphan transition + */ + orphanedAt?: string; + /** + * ISO 8601 timestamp of the most recent successful reclaim + */ + reclaimedAt?: string; + executionMode: TaskClientExecutionMode; + /** + * Whether the currently bound owner can receive a cancellation request + */ + canCancel: boolean; + /** + * Sequence number of the latest accepted owner update + */ + sequence: number; + /** + * Opaque successful terminal result supplied by the task owner + */ + result?: JsonValue; + /** + * Human-readable terminal failure message + */ + error?: string; + /** + * Optional owner-supplied terminal failure code + */ + errorCode?: string; + /** + * Human-readable reason for terminal cancellation + */ + cancellationReason?: string; +} +/** + * Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientOwner". + */ +/** @experimental */ +export interface TaskClientOwner { + /** + * Opaque session-scoped participant identity + */ + participantId: string; + /** + * Opaque identity of the currently or most recently bound session join + */ + joinId: string; + kind: TaskClientOwnerKind; + /** + * Display-only owner name + */ + displayName?: string; + /** + * Display-only owner source + */ + source?: string; + presence: TaskClientOwnerPresence; + /** + * ISO 8601 timestamp when the bound join disconnected + */ + disconnectedAt?: string; +} +/** + * Generic progress for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientProgress". + */ +/** @experimental */ +export interface TaskClientProgress { + type: TaskClientType; + status: TaskClientStatus; + /** + * Sequence number of the latest accepted owner update + */ + sequence: number; + /** + * ISO 8601 timestamp of the latest accepted lifecycle change + */ + updatedAt: string; + /** + * Current owner-defined progress phase + */ + phase?: string; + /** + * Current completion percentage from zero through one hundred + */ + percentage?: number; + /** + * Most recent nonempty progress message + */ + lastMessage?: string; + /** + * Recent server-timestamped progress messages + */ + recentActivity: TaskProgressLine[]; +} /** @experimental */ export interface TaskCompletionDecision { @@ -21057,6 +21527,54 @@ export interface TasksPromoteToBackgroundResult { */ /** @experimental */ export interface TasksRefreshResult {} +/** + * Registers or reclaims a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TasksRegisterRequest". + */ +/** @experimental */ +export interface TasksRegisterRequest { + type: TaskClientType; + /** + * Owner-scoped idempotency key used for registration and reclaim + */ + clientTaskId: string; + /** + * Human-readable description of the external work + */ + description: string; + /** + * Optional short display name for the external work + */ + displayName?: string; + /** + * Whether the owner supports runtime cancellation requests + */ + cancellable: boolean; + /** + * Expected current sequence for idempotent registration or orphan reclaim + */ + expectedSequence?: number; +} +/** + * Result of registering or reclaiming a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TasksRegisterResult". + */ +/** @experimental */ +export interface TasksRegisterResult { + task: TaskClientInfo; + /** + * True only when this invocation created a new task + */ + created: boolean; + /** + * True only when this invocation reclaimed an orphaned task + */ + reclaimed: boolean; +} /** * Identifier of the completed or cancelled task to remove from tracking. * @@ -21163,6 +21681,42 @@ export interface TasksStartAgentResult { */ agentId: string; } +/** + * Updates a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TasksUpdateRequest". + */ +/** @experimental */ +export interface TasksUpdateRequest { + /** + * Canonical runtime-generated task identifier + */ + id: string; + /** + * Owner update sequence to apply + */ + sequence: number; + update: TaskClientUpdate; +} +/** + * Result of publishing a client-owned task update. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TasksUpdateResult". + */ +/** @experimental */ +export interface TasksUpdateResult { + task: TaskClientInfo; + /** + * Whether this invocation changed task state + */ + applied: boolean; + /** + * Whether this invocation repeated the latest accepted update + */ + duplicate: boolean; +} /** * Wait until all in-flight background tasks (agents + shells) and any follow-up turns scheduled by their completions have settled. Returns when the runtime is fully drained or after an internal timeout (default 10 minutes; configurable via COPILOT_TASK_WAIT_TIMEOUT_SECONDS). * @@ -23523,6 +24077,11 @@ export function createServerRpc(connection: MessageConnection) { */ read: async (): Promise => connection.sendRequest("managedSettings.read", {}), + /** + * Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. + */ + clearCache: async (): Promise => + connection.sendRequest("managedSettings.clearCache", {}), }, /** @experimental */ runtime: { @@ -24160,9 +24719,9 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin /** @experimental */ model: { /** - * Gets the currently selected model for the session. + * Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. * - * @returns The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + * @returns The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. */ getCurrent: async (): Promise => connection.sendRequest("session.model.getCurrent", { sessionId }), @@ -24175,6 +24734,15 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ switchTo: async (params: ModelSwitchToRequest): Promise => connection.sendRequest("session.model.switchTo", { sessionId, ...params }), + /** + * Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. + * + * @param params An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + * + * @returns Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + */ + switchAutoTier: async (params: ModelSwitchAutoTierRequest): Promise => + connection.sendRequest("session.model.switchAutoTier", { sessionId, ...params }), /** * Updates the session's reasoning effort without changing the selected model. * @@ -24514,6 +25082,24 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ list: async (): Promise => connection.sendRequest("session.tasks.list", { sessionId }), + /** + * Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. + * + * @param params Registers or reclaims a client-owned task. + * + * @returns Result of registering or reclaiming a client-owned task. + */ + register: async (params: TasksRegisterRequest): Promise => + connection.sendRequest("session.tasks.register", { sessionId, ...params }), + /** + * Publishes generic progress or a terminal outcome for a client-owned task. + * + * @param params Updates a client-owned task. + * + * @returns Result of publishing a client-owned task update. + */ + update: async (params: TasksUpdateRequest): Promise => + connection.sendRequest("session.tasks.update", { sessionId, ...params }), /** * Refreshes metadata for any detached background shells the runtime knows about. * @@ -26192,6 +26778,19 @@ export interface FactoryHandler { abort(params: FactoryAbortRequest): Promise; } +/** Handler for `tasks` client session API methods. */ +/** @experimental */ +export interface TasksHandler { + /** + * Asks the client currently bound to a client-owned session task to confirm that its external work stopped. + * + * @param params Runtime-to-owner cancellation request for a client-owned task. + * + * @returns Whether the client authoritatively confirmed its external work stopped. + */ + cancel(params: ClientTaskCancelRequest): Promise; +} + /** Handler for `sessionFs` client session API methods. */ /** @experimental */ export interface SessionFsHandler { @@ -26332,6 +26931,7 @@ export interface CanvasHandler { export interface ClientSessionApiHandlers { providerToken?: ProviderTokenHandler; factory?: FactoryHandler; + tasks?: TasksHandler; sessionFs?: SessionFsHandler; canvas?: CanvasHandler; } @@ -26361,6 +26961,11 @@ export function registerClientSessionApiHandlers( if (!handler) throw new Error(`No factory handler registered for session: ${params.sessionId}`); return handler.abort(params); }); + connection.onRequest("tasks.cancel", async (params: ClientTaskCancelRequest) => { + const handler = getHandlers(params.sessionId).tasks; + if (!handler) throw new Error(`No tasks handler registered for session: ${params.sessionId}`); + return handler.cancel(params); + }); connection.onRequest("sessionFs.readFile", async (params: SessionFsReadFileRequest) => { const handler = getHandlers(params.sessionId).sessionFs; if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 7d734bf491..a61a143111 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -23,6 +23,7 @@ export type SessionEvent = | InfoEvent | WarningEvent | ModelChangeEvent + | AutoTierSwitchFailedEvent | ModeChangedEvent | ModeNoticeDeliveredEvent | SessionLimitsChangedEvent @@ -126,6 +127,8 @@ export type SessionEvent = | CustomAgentsUpdatedEvent | McpServersLoadedEvent | McpServerStatusChangedEvent + | McpServerRemovedEvent + | McpServerNeedsReconnectEvent | McpToolsListChangedEvent | McpResourcesListChangedEvent | McpPromptsListChangedEvent @@ -250,6 +253,18 @@ export type ModelChangeSource = | "automatic" /** An SDK or RPC caller selected the model. */ | "sdk"; +/** + * Terminal reason an Auto preference activation failed. + */ +export type AutoTierSwitchFailureReason = + /** The candidate model was rejected by model policy. */ + | "policy_rejected" + /** The Auto routing request failed or returned an unusable response. */ + | "request_failed" + /** The runtime could not prepare the Auto routing request. */ + | "setup_failed" + /** The provider does not support Auto routing. */ + | "unsupported"; /** * Permission mode for the session. */ @@ -1884,6 +1899,10 @@ export interface ModelChangeEvent { * Model change details including previous and new model identifiers */ export interface ModelChangeData { + /** + * Committed Auto preference after the model configuration change, when applicable. + */ + autoTier?: AutoTier | null; /** * Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. */ @@ -1896,6 +1915,7 @@ export interface ModelChangeData { * Newly selected model identifier */ newModel: string; + previousAutoTier?: AutoTier; /** * Model that was previously selected, if any */ @@ -1914,6 +1934,47 @@ export interface ModelChangeData { source?: ModelChangeSource; verbosity?: Verbosity; } +/** + * Session event "session.auto_tier_switch_failed". A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. + */ +export interface AutoTierSwitchFailedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: AutoTierSwitchFailedData; + /** + * 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 "session.auto_tier_switch_failed". + */ + type: "session.auto_tier_switch_failed"; +} +/** + * A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. + */ +export interface AutoTierSwitchFailedData { + effectiveAutoTier?: AutoTier; + reason: AutoTierSwitchFailureReason; + /** + * Auto preference that failed to activate, or null when returning to provider-default routing failed. + */ + requestedAutoTier: AutoTier | null; +} /** * Session event "session.mode_changed". Agent mode change details including previous and new modes */ @@ -11184,6 +11245,84 @@ export interface McpServerStatusChangedData { serverName: string; status: McpServerStatus; } +/** + * Session event "session.mcp_server_removed". Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. + */ +export interface McpServerRemovedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpServerRemovedData; + /** + * 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 "session.mcp_server_removed". + */ + type: "session.mcp_server_removed"; +} +/** + * Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. + */ +export interface McpServerRemovedData { + /** + * Name of the MCP server that was removed from the graph + */ + serverName: string; +} +/** + * Session event "session.mcp_server_needs_reconnect". Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. + */ +export interface McpServerNeedsReconnectEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpServerNeedsReconnectData; + /** + * 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 "session.mcp_server_needs_reconnect". + */ + type: "session.mcp_server_needs_reconnect"; +} +/** + * Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. + */ +export interface McpServerNeedsReconnectData { + /** + * Name of the MCP server that needs to reconnect + */ + serverName: string; +} /** * Session event "mcp.tools.list_changed". Payload identifying the MCP server associated with a list change. */ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 2abba2fcb6..098d9eac54 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -4323,6 +4323,7 @@ describe("connect handshake clientInfo", () => { const params = await captureConnectParams(); expect(params).not.toHaveProperty("clientInfo"); + expect(params.supportedTaskKinds).toEqual(["agent", "client", "shell"]); }); it("drops empty fields and omits an all-empty identity", async () => { diff --git a/nodejs/test/e2e/mcp_oauth.e2e.test.ts b/nodejs/test/e2e/mcp_oauth.e2e.test.ts index 5a00526b6d..0909afd8ad 100644 --- a/nodejs/test/e2e/mcp_oauth.e2e.test.ts +++ b/nodejs/test/e2e/mcp_oauth.e2e.test.ts @@ -59,6 +59,7 @@ describe("MCP OAuth host auth", async () => { }); onTestFinished(() => disconnectSession(session)); + await session.rpc.mcp.reload(); await waitForMcpServerStatus(session, serverName); const tools = await session.rpc.mcp.listTools({ serverName }); @@ -95,10 +96,7 @@ describe("MCP OAuth host auth", async () => { async () => { const oauthServer = await startOAuthMcpServer(); const serverName = "oauth-direct-rpc-mcp"; - let resolveAuthRequest!: (request: McpAuthRequest) => void; - const authRequest = new Promise((resolve) => { - resolveAuthRequest = resolve; - }); + const authRequests = createAsyncQueue(); let releaseHandler!: (value: unknown) => void; const handlerResult = new Promise((resolve) => { releaseHandler = resolve; @@ -108,7 +106,7 @@ describe("MCP OAuth host auth", async () => { onPermissionRequest: approveAll, enableMcpApps: true, onMcpAuthRequest: async (request) => { - resolveAuthRequest(request); + authRequests.push(request); await handlerResult; return { kind: "token", accessToken: EXPECTED_TOKEN }; }, @@ -124,8 +122,25 @@ describe("MCP OAuth host auth", async () => { }); onTestFinished(() => disconnectSession(session)); + const reload = session.rpc.mcp.reload(); const connected = waitForMcpServerStatus(session, serverName); - const request = await authRequest; + let request = await authRequests.next(); + while ( + !( + await session.rpc.mcp.oauth.handlePendingRequest({ + requestId: request.requestId, + result: { + kind: "token", + accessToken: EXPECTED_TOKEN, + tokenType: "Bearer", + expiresIn: 3600, + }, + }) + ).success + ) { + request = await authRequests.next(); + } + expect(request).toMatchObject({ requestId: expect.any(String), serverName, @@ -138,21 +153,11 @@ describe("MCP OAuth host auth", async () => { }, }); - const handled = await session.rpc.mcp.oauth.handlePendingRequest({ - requestId: request.requestId, - result: { - kind: "token", - accessToken: EXPECTED_TOKEN, - tokenType: "Bearer", - expiresIn: 3600, - }, - }); - expect(handled.success).toBe(true); - + releaseHandler(undefined); + await reload; await connected; const tools = await session.rpc.mcp.listTools({ serverName }); expect(tools.tools.map((tool) => tool.name)).toContain("whoami"); - releaseHandler(undefined); } ); @@ -197,18 +202,18 @@ describe("MCP OAuth host auth", async () => { }); onTestFinished(() => disconnectSession(session)); + await session.rpc.mcp.reload(); await waitForMcpServerStatus(session, serverName); + refreshCount = 0; 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", - ]); + expect( + authRequests + .filter((request) => request.reason !== "initial") + .map((request) => request.reason) + ).toEqual(["refresh", "upscope", "refresh", "reauth"]); const upscopeRequest = authRequests.find((request) => request.reason === "upscope"); expect(upscopeRequest?.wwwAuthenticateParams).toEqual({ @@ -263,6 +268,7 @@ describe("MCP OAuth host auth", async () => { }); onTestFinished(() => disconnectSession(session)); + await session.rpc.mcp.reload(); await waitForMcpServerStatus(session, serverName, "needs-auth"); expect(await authRequest).toMatchObject({ @@ -379,3 +385,24 @@ function stopChild(child: ChildProcessWithoutNullStreams): Promise { child.kill("SIGTERM"); return exitPromise; } + +function createAsyncQueue(): { push(value: T): void; next(): Promise } { + const values: T[] = []; + const waiters: Array<(value: T) => void> = []; + return { + push(value) { + const waiter = waiters.shift(); + if (waiter) { + waiter(value); + } else { + values.push(value); + } + }, + next() { + const value = values.shift(); + return value === undefined + ? new Promise((resolvePromise) => waiters.push(resolvePromise)) + : Promise.resolve(value); + }, + }; +} diff --git a/python/copilot/client.py b/python/copilot/client.py index 4a391e6a29..ab3410a56c 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -4102,7 +4102,9 @@ async def _verify_protocol_version(self) -> None: server_version: int | None try: - connect_params: dict[str, Any] = {} + connect_params: dict[str, Any] = { + "supportedTaskKinds": ["agent", "client", "shell"], + } if self._effective_connection_token is not None: connect_params["token"] = self._effective_connection_token # Opt in to GitHub telemetry forwarding at the connection level when a diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index b393fb6eed..d14fb4f561 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -1052,9 +1052,12 @@ class CapiSessionOptions: """Options scoped to the built-in CAPI (Copilot API) provider.""" auto_tier: AutoTier | None = None - """Routing preference used when the session model is `auto`. The runtime persists the - preference across cold resume. When omitted, the default routing behavior is used. - Resuming an already-resident session cannot change its preference. + """Routing preference for sessions whose model is `auto`. On create or cold resume, this + establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold + resume, the runtime restores the last committed preference. On resident resume, a + different value requests a safe switch after resume succeeds and cannot change an + in-flight turn. Successful switches are persisted for later cold resume. When no + preference is supplied or restored, CAPI default routing is used. """ enable_web_socket_responses: bool | None = None """Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when @@ -1497,6 +1500,34 @@ class CatalogUnsafeRetrievalReason(Enum): class CatalogUnsupportedKindErrorKind(Enum): UNSUPPORTED_KIND = "unsupported-kind" +# Experimental: this type is part of an experimental API and may change or be removed. +class ClientTaskCancelReason(Enum): + """Why the runtime requests client-task cancellation. + + Reason the runtime requests cancellation + """ + CANCEL_REQUESTED = "cancel_requested" + SESSION_SHUTDOWN = "session_shutdown" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ClientTaskCancelResult: + """Whether the client authoritatively confirmed its external work stopped.""" + + cancelled: bool + """True only when the owner confirms that external work stopped before responding""" + + @staticmethod + def from_dict(obj: Any) -> 'ClientTaskCancelResult': + assert isinstance(obj, dict) + cancelled = from_bool(obj.get("cancelled")) + return ClientTaskCancelResult(cancelled) + + def to_dict(self) -> dict: + result: dict = {} + result["cancelled"] = from_bool(self.cancelled) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandInputChoice: @@ -1832,34 +1863,14 @@ def to_dict(self) -> dict: return result # Experimental: this type is part of an experimental API and may change or be removed. -# Internal: this type is an internal SDK API and is not part of the public surface. -@dataclass -class _ConnectResult: - """Handshake result reporting the server's protocol version and package version on success.""" - - ok: bool - """Always true on success""" - - protocol_version: int - """Server protocol version number""" - - version: str - """Server package version""" - - @staticmethod - def from_dict(obj: Any) -> '_ConnectResult': - assert isinstance(obj, dict) - ok = from_bool(obj.get("ok")) - protocol_version = from_int(obj.get("protocolVersion")) - version = from_str(obj.get("version")) - return _ConnectResult(ok, protocol_version, version) +class TaskKind(Enum): + """Closed set of public task kinds a connection can negotiate. - def to_dict(self) -> dict: - result: dict = {} - result["ok"] = from_bool(self.ok) - result["protocolVersion"] = from_int(self.protocol_version) - result["version"] = from_str(self.version) - return result + Discriminator for a client-owned task. + """ + AGENT = "agent" + CLIENT = "client" + SHELL = "shell" # Experimental: this type is part of an experimental API and may change or be removed. class ConnectedRemoteSessionMetadataKind(Enum): @@ -2266,9 +2277,20 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CurrentModel: - """The currently selected model, reasoning effort, and context tier for the session. The - context tier reflects `Session.getContextTier()`, restored from the session journal on - resume. + """The session's authoritative model snapshot. Auto preference fields are configuration for + the virtual `auto` model and do not change the selected model identifier. The context + tier reflects `Session.getContextTier()`, restored from the session journal on resume. + + Authoritative model and Auto preference state after an immediate switch. For deferred + switches this remains the current state until the queued change drains. + """ + activating_auto_tier: AutoTier | None = None + """Auto preference currently claimed by an in-progress activation. Null means the activation + is returning to provider-default routing. + """ + auto_tier: AutoTier | None = None + """Auto preference currently committed for the session. This can remain available while + another model is selected so a later switch to `auto` can reuse it. """ context_tier: ContextTier | None = None """Context tier for models that support multiple context-window sizes.""" @@ -2276,6 +2298,10 @@ class CurrentModel: model_id: str | None = None """Currently active model identifier""" + pending_auto_tier: AutoTier | None = None + """Latest unclaimed Auto preference waiting for a future user turn. Null means the pending + request is returning to provider-default routing. + """ reasoning_effort: str | None = None """Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the @@ -2285,17 +2311,26 @@ class CurrentModel: @staticmethod def from_dict(obj: Any) -> 'CurrentModel': assert isinstance(obj, dict) + activating_auto_tier = from_union([AutoTier, from_none], obj.get("activatingAutoTier")) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) context_tier = from_union([ContextTier, from_none], obj.get("contextTier")) model_id = from_union([from_str, from_none], obj.get("modelId")) + pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) - return CurrentModel(context_tier, model_id, reasoning_effort) + return CurrentModel(activating_auto_tier, auto_tier, context_tier, model_id, pending_auto_tier, reasoning_effort) def to_dict(self) -> dict: result: dict = {} + if self.activating_auto_tier is not None: + result["activatingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.activating_auto_tier) + if self.auto_tier is not None: + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) if self.context_tier is not None: result["contextTier"] = from_union([lambda x: to_enum(ContextTier, x), from_none], self.context_tier) if self.model_id is not None: result["modelId"] = from_union([from_str, from_none], self.model_id) + if self.pending_auto_tier is not None: + result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) return result @@ -5071,15 +5106,21 @@ class MCPConfigRemoveRequest: name: str """Name of the MCP server to remove""" + auth_client_id_metadata_url: str | None = None + """OAuth Client ID Metadata Document URL whose persisted credentials should also be removed.""" + @staticmethod def from_dict(obj: Any) -> 'MCPConfigRemoveRequest': assert isinstance(obj, dict) name = from_str(obj.get("name")) - return MCPConfigRemoveRequest(name) + auth_client_id_metadata_url = from_union([from_str, from_none], obj.get("authClientIdMetadataUrl")) + return MCPConfigRemoveRequest(name, auth_client_id_metadata_url) def to_dict(self) -> dict: result: dict = {} result["name"] = from_str(self.name) + if self.auth_client_id_metadata_url is not None: + result["authClientIdMetadataUrl"] = from_union([from_str, from_none], self.auth_client_id_metadata_url) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6925,6 +6966,45 @@ def to_dict(self) -> dict: result["reasoningEffort"] = from_str(self.reasoning_effort) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelSwitchAutoTierRequest: + """An Auto preference request for the session. This updates Auto configuration only; it does + not change the selected model to `auto`. + """ + auto_tier: AutoTier | None = None + """Auto preference to activate when a future user turn using the `auto` model safely mints a + replacement model and token pair. Pass null to return to provider-default Auto routing. + """ + source: ModelChangeSource | None = None + """Origin to record on the effective `session.model_change` event. Defaults to `sdk` when + omitted. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelSwitchAutoTierRequest': + assert isinstance(obj, dict) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) + source = from_union([ModelChangeSource, from_none], obj.get("source")) + return ModelSwitchAutoTierRequest(auto_tier, source) + + def to_dict(self) -> dict: + result: dict = {} + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) + if self.source is not None: + result["source"] = from_union([lambda x: to_enum(ModelChangeSource, x), from_none], self.source) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelSwitchAutoTierStatus(Enum): + """Immediate request status. `pending` means accepted but not committed. + + Whether the requested preference was already effective or was accepted for later + transactional activation. + """ + PENDING = "pending" + UNCHANGED = "unchanged" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelsListRequest: @@ -13087,8 +13167,12 @@ class SubagentSettingsEntryContextTier(Enum): # Experimental: this type is part of an experimental API and may change or be removed. class TaskExecutionMode(Enum): - """Whether task execution is synchronously awaited or managed in the background""" + """Whether task execution is synchronously awaited or managed in the background + + Client-owned tasks always execute outside the runtime in background mode. + Execution mode, which is always background for client-owned tasks + """ BACKGROUND = "background" SYNC = "sync" @@ -13129,6 +13213,70 @@ def to_dict(self) -> dict: result["timestamp"] = self.timestamp.isoformat() return result +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientActiveStatus(Enum): + """Active status a client owner may publish with a progress update. + + Optional active status transition + """ + IDLE = "idle" + RUNNING = "running" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientExecutionMode(Enum): + """Client-owned tasks always execute outside the runtime in background mode. + + Execution mode, which is always background for client-owned tasks + """ + BACKGROUND = "background" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientOwnerKind(Enum): + """Class of the task owner + + Connection class owning a client task. + """ + EXTENSION = "extension" + SDK = "sdk" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientOwnerPresence(Enum): + """Whether this task's bound join is currently connected + + Presence of the task's bound join. + """ + CONNECTED = "connected" + DISCONNECTED = "disconnected" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientStatus(Enum): + """Client task lifecycle status + + Lifecycle status of a client-owned task. + + Current client task lifecycle status + + Current lifecycle status of the task + """ + CANCELLED = "cancelled" + COMPLETED = "completed" + FAILED = "failed" + IDLE = "idle" + ORPHANED = "orphaned" + RUNNING = "running" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientType(Enum): + """Discriminator for a client-owned task.""" + + CLIENT = "client" + +class TaskClientUpdateKind(Enum): + CANCELLED = "cancelled" + COMPLETED = "completed" + FAILED = "failed" + PROGRESS = "progress" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TaskCompleteData: @@ -13240,10 +13388,6 @@ class TaskShellInfoAttachmentMode(Enum): ATTACHED = "attached" DETACHED = "detached" -class TaskInfoType(Enum): - AGENT = "agent" - SHELL = "shell" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TaskList: @@ -15795,6 +15939,45 @@ def to_dict(self) -> dict: result["supportedKinds"] = from_list(lambda x: to_enum(CatalogCandidateKind, x), self.supported_kinds) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ClientTaskCancelRequest: + """Runtime-to-owner cancellation request for a client-owned task.""" + + cancellation_id: str + """Opaque identifier shared by coalesced cancellation callers""" + + client_task_id: str + """Owner-scoped task key included for correlation""" + + id: str + """Canonical runtime-generated task identifier""" + + reason: ClientTaskCancelReason + """Reason the runtime requests cancellation""" + + session_id: str + """Session that owns the client task""" + + @staticmethod + def from_dict(obj: Any) -> 'ClientTaskCancelRequest': + assert isinstance(obj, dict) + cancellation_id = from_str(obj.get("cancellationId")) + client_task_id = from_str(obj.get("clientTaskId")) + id = from_str(obj.get("id")) + reason = ClientTaskCancelReason(obj.get("reason")) + session_id = from_str(obj.get("sessionId")) + return ClientTaskCancelRequest(cancellation_id, client_task_id, id, reason, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["cancellationId"] = from_str(self.cancellation_id) + result["clientTaskId"] = from_str(self.client_task_id) + result["id"] = from_str(self.id) + result["reason"] = to_enum(ClientTaskCancelReason, self.reason) + result["sessionId"] = from_str(self.session_id) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandInput: @@ -15948,6 +16131,10 @@ class _ConnectRequest: using the process-global gate for ordinary events and an explicit session-scoped decision for host-only events. """ + supported_task_kinds: list[TaskKind] | None = None + """Task kinds this connection can decode when observing session tasks. Omit to retain agent + and shell compatibility. + """ token: str | None = None """Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN""" @@ -15956,8 +16143,9 @@ def from_dict(obj: Any) -> '_ConnectRequest': assert isinstance(obj, dict) client_info = from_union([_ConnectClientInfo.from_dict, from_none], obj.get("clientInfo")) enable_git_hub_telemetry_forwarding = from_union([from_bool, from_none], obj.get("enableGitHubTelemetryForwarding")) + supported_task_kinds = from_union([lambda x: from_list(TaskKind, x), from_none], obj.get("supportedTaskKinds")) token = from_union([from_str, from_none], obj.get("token")) - return _ConnectRequest(client_info, enable_git_hub_telemetry_forwarding, token) + return _ConnectRequest(client_info, enable_git_hub_telemetry_forwarding, supported_task_kinds, token) def to_dict(self) -> dict: result: dict = {} @@ -15965,10 +16153,48 @@ def to_dict(self) -> dict: result["clientInfo"] = from_union([lambda x: to_class(_ConnectClientInfo, x), from_none], self.client_info) if self.enable_git_hub_telemetry_forwarding is not None: result["enableGitHubTelemetryForwarding"] = from_union([from_bool, from_none], self.enable_git_hub_telemetry_forwarding) + if self.supported_task_kinds is not None: + result["supportedTaskKinds"] = from_union([lambda x: from_list(lambda x: to_enum(TaskKind, x), x), from_none], self.supported_task_kinds) if self.token is not None: result["token"] = from_union([from_str, from_none], self.token) return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _ConnectResult: + """Handshake result reporting the server's protocol version and package version on success.""" + + ok: bool + """Always true on success""" + + protocol_version: int + """Server protocol version number""" + + version: str + """Server package version""" + + task_kinds: list[TaskKind] | None = None + """Task kinds the server may return to this connection.""" + + @staticmethod + def from_dict(obj: Any) -> '_ConnectResult': + assert isinstance(obj, dict) + ok = from_bool(obj.get("ok")) + protocol_version = from_int(obj.get("protocolVersion")) + version = from_str(obj.get("version")) + task_kinds = from_union([lambda x: from_list(TaskKind, x), from_none], obj.get("taskKinds")) + return _ConnectResult(ok, protocol_version, version, task_kinds) + + def to_dict(self) -> dict: + result: dict = {} + result["ok"] = from_bool(self.ok) + result["protocolVersion"] = from_int(self.protocol_version) + result["version"] = from_str(self.version) + if self.task_kinds is not None: + result["taskKinds"] = from_union([lambda x: from_list(lambda x: to_enum(TaskKind, x), x), from_none], self.task_kinds) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ConnectedRemoteSessionMetadata: @@ -19945,6 +20171,10 @@ class ModelSwitchToResult: model_id: str | None = None """Currently active model identifier after the switch""" + model_state: CurrentModel | None = None + """Authoritative model and Auto preference state after an immediate switch. For deferred + switches this remains the current state until the queued change drains. + """ persistence_error: str | None = None """Persistence failure encountered after applying the model switch.""" @@ -19962,10 +20192,11 @@ def from_dict(obj: Any) -> 'ModelSwitchToResult': deprecation_warnings = from_union([lambda x: from_list(from_str, x), from_none], obj.get("deprecationWarnings")) message = from_union([from_str, from_none], obj.get("message")) model_id = from_union([from_str, from_none], obj.get("modelId")) + model_state = from_union([CurrentModel.from_dict, from_none], obj.get("modelState")) persistence_error = from_union([from_str, from_none], obj.get("persistenceError")) status = from_union([from_str, from_none], obj.get("status")) warning = from_union([from_str, from_none], obj.get("warning")) - return ModelSwitchToResult(confirmation, deferred, deprecation_warnings, message, model_id, persistence_error, status, warning) + return ModelSwitchToResult(confirmation, deferred, deprecation_warnings, message, model_id, model_state, persistence_error, status, warning) def to_dict(self) -> dict: result: dict = {} @@ -19979,6 +20210,8 @@ def to_dict(self) -> dict: result["message"] = from_union([from_str, from_none], self.message) if self.model_id is not None: result["modelId"] = from_union([from_str, from_none], self.model_id) + if self.model_state is not None: + result["modelState"] = from_union([lambda x: to_class(CurrentModel, x), from_none], self.model_state) if self.persistence_error is not None: result["persistenceError"] = from_union([from_str, from_none], self.persistence_error) if self.status is not None: @@ -20188,6 +20421,53 @@ def to_dict(self) -> dict: result["vision"] = from_union([lambda x: to_class(ModelCapabilitiesOverrideLimitsVision, x), from_none], self.vision) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelSwitchAutoTierResult: + """Immediate acknowledgement and Auto preference snapshot after a switch request. This + result never implies that a pending preference committed. + """ + status: ModelSwitchAutoTierStatus + """Immediate request status. `pending` means accepted but not committed.""" + + activating_auto_tier: AutoTier | None = None + """Auto preference currently claimed by an in-progress activation. Null means the activation + is returning to provider-default routing. + """ + effective_auto_tier: AutoTier | None = None + """Auto preference currently committed for the session.""" + + pending_auto_tier: AutoTier | None = None + """Latest unclaimed Auto preference waiting for a future user turn.""" + + superseded_auto_tier: AutoTier | None = None + """Earlier unclaimed preference replaced by this request. This can be present with either + status, including when selecting the effective preference cancels pending work. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelSwitchAutoTierResult': + assert isinstance(obj, dict) + status = ModelSwitchAutoTierStatus(obj.get("status")) + activating_auto_tier = from_union([AutoTier, from_none], obj.get("activatingAutoTier")) + effective_auto_tier = from_union([AutoTier, from_none], obj.get("effectiveAutoTier")) + pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) + superseded_auto_tier = from_union([AutoTier, from_none], obj.get("supersededAutoTier")) + return ModelSwitchAutoTierResult(status, activating_auto_tier, effective_auto_tier, pending_auto_tier, superseded_auto_tier) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(ModelSwitchAutoTierStatus, self.status) + if self.activating_auto_tier is not None: + result["activatingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.activating_auto_tier) + if self.effective_auto_tier is not None: + result["effectiveAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.effective_auto_tier) + if self.pending_auto_tier is not None: + result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) + if self.superseded_auto_tier is not None: + result["supersededAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.superseded_auto_tier) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class NamedProviderConfig: @@ -24218,23 +24498,100 @@ def to_dict(self) -> dict: result["latestIntent"] = from_union([from_str, from_none], self.latest_intent) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskClientOwner: + """Public attribution and presence for the task owner + + Public owner attribution for a client-owned task. Identifiers are opaque and never + authorize requests. + """ + join_id: str + """Opaque identity of the currently or most recently bound session join""" + + kind: TaskClientOwnerKind + """Class of the task owner""" + + participant_id: str + """Opaque session-scoped participant identity""" + + presence: TaskClientOwnerPresence + """Whether this task's bound join is currently connected""" + + disconnected_at: datetime | None = None + """ISO 8601 timestamp when the bound join disconnected""" + + display_name: str | None = None + """Display-only owner name""" + + source: str | None = None + """Display-only owner source""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskClientOwner': + assert isinstance(obj, dict) + join_id = from_str(obj.get("joinId")) + kind = TaskClientOwnerKind(obj.get("kind")) + participant_id = from_str(obj.get("participantId")) + presence = TaskClientOwnerPresence(obj.get("presence")) + disconnected_at = from_union([from_datetime, from_none], obj.get("disconnectedAt")) + display_name = from_union([from_str, from_none], obj.get("displayName")) + source = from_union([from_str, from_none], obj.get("source")) + return TaskClientOwner(join_id, kind, participant_id, presence, disconnected_at, display_name, source) + + def to_dict(self) -> dict: + result: dict = {} + result["joinId"] = from_str(self.join_id) + result["kind"] = to_enum(TaskClientOwnerKind, self.kind) + result["participantId"] = from_str(self.participant_id) + result["presence"] = to_enum(TaskClientOwnerPresence, self.presence) + if self.disconnected_at is not None: + result["disconnectedAt"] = from_union([lambda x: x.isoformat(), from_none], self.disconnected_at) + if self.display_name is not None: + result["displayName"] = from_union([from_str, from_none], self.display_name) + if self.source is not None: + result["source"] = from_union([from_str, from_none], self.source) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TaskProgress: """Progress snapshot for an agent task, with recent activity lines and optional latest intent. + Generic progress for a client-owned task. + Progress snapshot for a shell task, with recent stdout/stderr output and optional process ID. """ - type: TaskInfoType + type: TaskKind """Progress kind""" latest_intent: str | None = None """The most recent intent reported by the agent""" recent_activity: list[TaskProgressLine] | None = None - """Recent tool execution events converted to display lines""" + """Recent tool execution events converted to display lines + + Recent server-timestamped progress messages + """ + last_message: str | None = None + """Most recent nonempty progress message""" + + percentage: float | None = None + """Current completion percentage from zero through one hundred""" + + phase: str | None = None + """Current owner-defined progress phase""" + + sequence: int | None = None + """Sequence number of the latest accepted owner update""" + + status: TaskClientStatus | None = None + """Current client task lifecycle status""" + + updated_at: datetime | None = None + """ISO 8601 timestamp of the latest accepted lifecycle change""" pid: int | None = None """Process ID when available""" @@ -24245,26 +24602,226 @@ class TaskProgress: @staticmethod def from_dict(obj: Any) -> 'TaskProgress': assert isinstance(obj, dict) - type = TaskInfoType(obj.get("type")) + type = TaskKind(obj.get("type")) latest_intent = from_union([from_str, from_none], obj.get("latestIntent")) recent_activity = from_union([lambda x: from_list(TaskProgressLine.from_dict, x), from_none], obj.get("recentActivity")) + last_message = from_union([from_str, from_none], obj.get("lastMessage")) + percentage = from_union([from_float, from_none], obj.get("percentage")) + phase = from_union([from_str, from_none], obj.get("phase")) + sequence = from_union([from_int, from_none], obj.get("sequence")) + status = from_union([TaskClientStatus, from_none], obj.get("status")) + updated_at = from_union([from_datetime, from_none], obj.get("updatedAt")) pid = from_union([from_int, from_none], obj.get("pid")) recent_output = from_union([from_str, from_none], obj.get("recentOutput")) - return TaskProgress(type, latest_intent, recent_activity, pid, recent_output) + return TaskProgress(type, latest_intent, recent_activity, last_message, percentage, phase, sequence, status, updated_at, pid, recent_output) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(TaskInfoType, self.type) + result["type"] = to_enum(TaskKind, self.type) if self.latest_intent is not None: result["latestIntent"] = from_union([from_str, from_none], self.latest_intent) if self.recent_activity is not None: result["recentActivity"] = from_union([lambda x: from_list(lambda x: to_class(TaskProgressLine, x), x), from_none], self.recent_activity) + if self.last_message is not None: + result["lastMessage"] = from_union([from_str, from_none], self.last_message) + if self.percentage is not None: + result["percentage"] = from_union([to_float, from_none], self.percentage) + if self.phase is not None: + result["phase"] = from_union([from_str, from_none], self.phase) + if self.sequence is not None: + result["sequence"] = from_union([from_int, from_none], self.sequence) + if self.status is not None: + result["status"] = from_union([lambda x: to_enum(TaskClientStatus, x), from_none], self.status) + if self.updated_at is not None: + result["updatedAt"] = from_union([lambda x: x.isoformat(), from_none], self.updated_at) if self.pid is not None: result["pid"] = from_union([from_int, from_none], self.pid) if self.recent_output is not None: result["recentOutput"] = from_union([from_str, from_none], self.recent_output) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskClientProgress: + """Generic progress for a client-owned task.""" + + recent_activity: list[TaskProgressLine] + """Recent server-timestamped progress messages""" + + sequence: int + """Sequence number of the latest accepted owner update""" + + status: TaskClientStatus + """Current client task lifecycle status""" + + type: TaskClientType + """Progress kind""" + + updated_at: datetime + """ISO 8601 timestamp of the latest accepted lifecycle change""" + + last_message: str | None = None + """Most recent nonempty progress message""" + + percentage: float | None = None + """Current completion percentage from zero through one hundred""" + + phase: str | None = None + """Current owner-defined progress phase""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskClientProgress': + assert isinstance(obj, dict) + recent_activity = from_list(TaskProgressLine.from_dict, obj.get("recentActivity")) + sequence = from_int(obj.get("sequence")) + status = TaskClientStatus(obj.get("status")) + type = TaskClientType(obj.get("type")) + updated_at = from_datetime(obj.get("updatedAt")) + last_message = from_union([from_str, from_none], obj.get("lastMessage")) + percentage = from_union([from_float, from_none], obj.get("percentage")) + phase = from_union([from_str, from_none], obj.get("phase")) + return TaskClientProgress(recent_activity, sequence, status, type, updated_at, last_message, percentage, phase) + + def to_dict(self) -> dict: + result: dict = {} + result["recentActivity"] = from_list(lambda x: to_class(TaskProgressLine, x), self.recent_activity) + result["sequence"] = from_int(self.sequence) + result["status"] = to_enum(TaskClientStatus, self.status) + result["type"] = to_enum(TaskClientType, self.type) + result["updatedAt"] = self.updated_at.isoformat() + if self.last_message is not None: + result["lastMessage"] = from_union([from_str, from_none], self.last_message) + if self.percentage is not None: + result["percentage"] = from_union([to_float, from_none], self.percentage) + if self.phase is not None: + result["phase"] = from_union([from_str, from_none], self.phase) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksRegisterRequest: + """Registers or reclaims a client-owned task.""" + + cancellable: bool + """Whether the owner supports runtime cancellation requests""" + + client_task_id: str + """Owner-scoped idempotency key used for registration and reclaim""" + + description: str + """Human-readable description of the external work""" + + type: TaskClientType + """Task kind""" + + display_name: str | None = None + """Optional short display name for the external work""" + + expected_sequence: int | None = None + """Expected current sequence for idempotent registration or orphan reclaim""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksRegisterRequest': + assert isinstance(obj, dict) + cancellable = from_bool(obj.get("cancellable")) + client_task_id = from_str(obj.get("clientTaskId")) + description = from_str(obj.get("description")) + type = TaskClientType(obj.get("type")) + display_name = from_union([from_str, from_none], obj.get("displayName")) + expected_sequence = from_union([from_int, from_none], obj.get("expectedSequence")) + return TasksRegisterRequest(cancellable, client_task_id, description, type, display_name, expected_sequence) + + def to_dict(self) -> dict: + result: dict = {} + result["cancellable"] = from_bool(self.cancellable) + result["clientTaskId"] = from_str(self.client_task_id) + result["description"] = from_str(self.description) + result["type"] = to_enum(TaskClientType, self.type) + if self.display_name is not None: + result["displayName"] = from_union([from_str, from_none], self.display_name) + if self.expected_sequence is not None: + result["expectedSequence"] = from_union([from_int, from_none], self.expected_sequence) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskClientUpdate: + """Progress or terminal update for a client-owned task. + + Progress or terminal update payload + + Publishes nonterminal progress for a running or idle client task. + + Reports successful terminal completion. + + Reports terminal failure. + + Reports terminal cancellation after external work stopped. + """ + kind: TaskClientUpdateKind + """Client task update variant discriminator.""" + + message: str | None = None + """Optional progress message appended to recent activity when nonempty + + Optional final progress message + """ + percentage: float | None = None + """Optional completion percentage; null clears the current percentage""" + + phase: str | None = None + """Optional progress phase; null clears the current phase""" + + status: TaskClientActiveStatus | None = None + """Optional active status transition""" + + result: Any = None + """Optional opaque successful terminal result""" + + code: str | None = None + """Optional owner-supplied terminal failure code""" + + error: str | None = None + """Human-readable terminal failure message""" + + reason: str | None = None + """Optional human-readable cancellation reason""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskClientUpdate': + assert isinstance(obj, dict) + kind = TaskClientUpdateKind(obj.get("kind")) + message = from_union([from_str, from_none], obj.get("message")) + percentage = from_union([from_float, from_none], obj.get("percentage")) + phase = from_union([from_none, from_str], obj.get("phase")) + status = from_union([TaskClientActiveStatus, from_none], obj.get("status")) + result = obj.get("result") + code = from_union([from_str, from_none], obj.get("code")) + error = from_union([from_str, from_none], obj.get("error")) + reason = from_union([from_str, from_none], obj.get("reason")) + return TaskClientUpdate(kind, message, percentage, phase, status, result, code, error, reason) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(TaskClientUpdateKind, self.kind) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.percentage is not None: + result["percentage"] = from_union([to_float, from_none], self.percentage) + if self.phase is not None: + result["phase"] = from_union([from_none, from_str], self.phase) + if self.status is not None: + result["status"] = from_union([lambda x: to_enum(TaskClientActiveStatus, x), from_none], self.status) + if self.result is not None: + result["result"] = self.result + if self.code is not None: + result["code"] = from_union([from_str, from_none], self.code) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.reason is not None: + result["reason"] = from_union([from_str, from_none], self.reason) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TaskShellInfo: @@ -28541,6 +29098,143 @@ def to_dict(self) -> dict: result["paths"] = from_list(lambda x: to_class(SkillDiscoveryPath, x), self.paths) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskClientInfo: + """Tracked client-owned task metadata. + + Authoritative registered or reclaimed task + + Authoritative task after processing the update + """ + active_time_ms: int + """Accumulated active execution time in milliseconds""" + + can_cancel: bool + """Whether the currently bound owner can receive a cancellation request""" + + client_task_id: str + """Owner-scoped registration and reclaim key""" + + description: str + """Task description""" + + execution_mode: TaskClientExecutionMode + """Execution mode, which is always background for client-owned tasks""" + + id: str + """Canonical runtime-generated task identifier""" + + owner: TaskClientOwner + """Public attribution and presence for the task owner""" + + sequence: int + """Sequence number of the latest accepted owner update""" + + started_at: datetime + """ISO 8601 timestamp when the task started""" + + status: TaskClientStatus + """Client task lifecycle status""" + + type: ClassVar[str] = "client" + """Task kind""" + + updated_at: datetime + """ISO 8601 timestamp of the latest accepted lifecycle change""" + + active_started_at: datetime | None = None + """ISO 8601 timestamp when the current active segment started""" + + cancellation_reason: str | None = None + """Human-readable reason for terminal cancellation""" + + completed_at: datetime | None = None + """ISO 8601 timestamp when the task reached a terminal status""" + + display_name: str | None = None + """Optional task display name""" + + error: str | None = None + """Human-readable terminal failure message""" + + error_code: str | None = None + """Optional owner-supplied terminal failure code""" + + idle_since: datetime | None = None + """ISO 8601 timestamp when the connected owner entered idle status""" + + orphaned_at: datetime | None = None + """ISO 8601 timestamp of the most recent orphan transition""" + + reclaimed_at: datetime | None = None + """ISO 8601 timestamp of the most recent successful reclaim""" + + result: Any = None + """Opaque successful terminal result supplied by the task owner""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskClientInfo': + assert isinstance(obj, dict) + active_time_ms = from_int(obj.get("activeTimeMs")) + can_cancel = from_bool(obj.get("canCancel")) + client_task_id = from_str(obj.get("clientTaskId")) + description = from_str(obj.get("description")) + execution_mode = TaskClientExecutionMode(obj.get("executionMode")) + id = from_str(obj.get("id")) + owner = TaskClientOwner.from_dict(obj.get("owner")) + sequence = from_int(obj.get("sequence")) + started_at = from_datetime(obj.get("startedAt")) + status = TaskClientStatus(obj.get("status")) + updated_at = from_datetime(obj.get("updatedAt")) + active_started_at = from_union([from_datetime, from_none], obj.get("activeStartedAt")) + cancellation_reason = from_union([from_str, from_none], obj.get("cancellationReason")) + completed_at = from_union([from_datetime, from_none], obj.get("completedAt")) + display_name = from_union([from_str, from_none], obj.get("displayName")) + error = from_union([from_str, from_none], obj.get("error")) + error_code = from_union([from_str, from_none], obj.get("errorCode")) + idle_since = from_union([from_datetime, from_none], obj.get("idleSince")) + orphaned_at = from_union([from_datetime, from_none], obj.get("orphanedAt")) + reclaimed_at = from_union([from_datetime, from_none], obj.get("reclaimedAt")) + result = obj.get("result") + return TaskClientInfo(active_time_ms, can_cancel, client_task_id, description, execution_mode, id, owner, sequence, started_at, status, updated_at, active_started_at, cancellation_reason, completed_at, display_name, error, error_code, idle_since, orphaned_at, reclaimed_at, result) + + def to_dict(self) -> dict: + result: dict = {} + result["activeTimeMs"] = from_int(self.active_time_ms) + result["canCancel"] = from_bool(self.can_cancel) + result["clientTaskId"] = from_str(self.client_task_id) + result["description"] = from_str(self.description) + result["executionMode"] = to_enum(TaskClientExecutionMode, self.execution_mode) + result["id"] = from_str(self.id) + result["owner"] = to_class(TaskClientOwner, self.owner) + result["sequence"] = from_int(self.sequence) + result["startedAt"] = self.started_at.isoformat() + result["status"] = to_enum(TaskClientStatus, self.status) + result["type"] = self.type + result["updatedAt"] = self.updated_at.isoformat() + if self.active_started_at is not None: + result["activeStartedAt"] = from_union([lambda x: x.isoformat(), from_none], self.active_started_at) + if self.cancellation_reason is not None: + result["cancellationReason"] = from_union([from_str, from_none], self.cancellation_reason) + if self.completed_at is not None: + result["completedAt"] = from_union([lambda x: x.isoformat(), from_none], self.completed_at) + if self.display_name is not None: + result["displayName"] = from_union([from_str, from_none], self.display_name) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.error_code is not None: + result["errorCode"] = from_union([from_str, from_none], self.error_code) + if self.idle_since is not None: + result["idleSince"] = from_union([lambda x: x.isoformat(), from_none], self.idle_since) + if self.orphaned_at is not None: + result["orphanedAt"] = from_union([lambda x: x.isoformat(), from_none], self.orphaned_at) + if self.reclaimed_at is not None: + result["reclaimedAt"] = from_union([lambda x: x.isoformat(), from_none], self.reclaimed_at) + if self.result is not None: + result["result"] = self.result + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TasksGetProgressResult: @@ -28563,6 +29257,35 @@ def to_dict(self) -> dict: result["progress"] = from_union([lambda x: to_class(TaskProgress, x), from_none], self.progress) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksUpdateRequest: + """Updates a client-owned task.""" + + id: str + """Canonical runtime-generated task identifier""" + + sequence: int + """Owner update sequence to apply""" + + update: TaskClientUpdate + """Progress or terminal update payload""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksUpdateRequest': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + sequence = from_int(obj.get("sequence")) + update = TaskClientUpdate.from_dict(obj.get("update")) + return TasksUpdateRequest(id, sequence, update) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["sequence"] = from_int(self.sequence) + result["update"] = to_class(TaskClientUpdate, self.update) + return result + # Experimental: this type is part of an experimental API and may change or be removed. # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass @@ -30767,6 +31490,13 @@ class SandboxConfig: add_current_working_directory: bool | None = None """Whether to auto-add the current working directory to readwritePaths. Default: true.""" + allow_bypass: bool | None = None + """Whether the agent may request that an individual command run outside the sandbox, which + the host then approves or denies through the usual permission flow. A host capability + flag rather than part of the policy: it is stripped from the effective spawn policy and + only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this + object: omitting it offers no bypass. Default: false (opt-in). + """ allow_dev_tool_access: bool | None = None """Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common @@ -30783,6 +31513,29 @@ class SandboxConfig: auth: SandboxConfigAuth | None = None """Credential-injection capability flags.""" + # Internal: this field is an internal SDK API and is not part of the public surface. + managed_lsp_routing_locked: bool | None = None + """The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`.""" + + # Internal: this field is an internal SDK API and is not part of the public surface. + managed_mcp_routing_locked: bool | None = None + """Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local + opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at + the administrator instead of a setting the next managed merge would override, and it is + ignored when comparing two configs for change. Only the managed merge may set it; a + caller-supplied value is stripped. + """ + sandbox_lsp_servers: bool | None = None + """Whether language servers the session launches are confined by the sandbox. Only an + explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by + default; set to false to opt out). + """ + sandbox_mcp_servers: bool | None = None + """Whether MCP servers the session launches are confined by the sandbox. Only an explicit + `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and + `enabled` are always read together. Ignored while `enabled` is false. Default: true + (enabled by default; set to false to opt out). + """ user_policy: SandboxConfigUserPolicy | None = None """User-managed sandbox policy fragment merged into the auto-discovered base policy.""" @@ -30791,20 +31544,35 @@ def from_dict(obj: Any) -> 'SandboxConfig': assert isinstance(obj, dict) enabled = from_bool(obj.get("enabled")) add_current_working_directory = from_union([from_bool, from_none], obj.get("addCurrentWorkingDirectory")) + allow_bypass = from_union([from_bool, from_none], obj.get("allowBypass")) allow_dev_tool_access = from_union([from_bool, from_none], obj.get("allowDevToolAccess")) auth = from_union([SandboxConfigAuth.from_dict, from_none], obj.get("auth")) + managed_lsp_routing_locked = from_union([from_bool, from_none], obj.get("managedLspRoutingLocked")) + managed_mcp_routing_locked = from_union([from_bool, from_none], obj.get("managedMcpRoutingLocked")) + sandbox_lsp_servers = from_union([from_bool, from_none], obj.get("sandboxLspServers")) + sandbox_mcp_servers = from_union([from_bool, from_none], obj.get("sandboxMcpServers")) user_policy = from_union([SandboxConfigUserPolicy.from_dict, from_none], obj.get("userPolicy")) - return SandboxConfig(enabled, add_current_working_directory, allow_dev_tool_access, auth, user_policy) + return SandboxConfig(enabled, add_current_working_directory, allow_bypass, allow_dev_tool_access, auth, managed_lsp_routing_locked, managed_mcp_routing_locked, sandbox_lsp_servers, sandbox_mcp_servers, user_policy) def to_dict(self) -> dict: result: dict = {} result["enabled"] = from_bool(self.enabled) if self.add_current_working_directory is not None: result["addCurrentWorkingDirectory"] = from_union([from_bool, from_none], self.add_current_working_directory) + if self.allow_bypass is not None: + result["allowBypass"] = from_union([from_bool, from_none], self.allow_bypass) if self.allow_dev_tool_access is not None: result["allowDevToolAccess"] = from_union([from_bool, from_none], self.allow_dev_tool_access) if self.auth is not None: result["auth"] = from_union([lambda x: to_class(SandboxConfigAuth, x), from_none], self.auth) + if self.managed_lsp_routing_locked is not None: + result["managedLspRoutingLocked"] = from_union([from_bool, from_none], self.managed_lsp_routing_locked) + if self.managed_mcp_routing_locked is not None: + result["managedMcpRoutingLocked"] = from_union([from_bool, from_none], self.managed_mcp_routing_locked) + if self.sandbox_lsp_servers is not None: + result["sandboxLspServers"] = from_union([from_bool, from_none], self.sandbox_lsp_servers) + if self.sandbox_mcp_servers is not None: + result["sandboxMcpServers"] = from_union([from_bool, from_none], self.sandbox_mcp_servers) if self.user_policy is not None: result["userPolicy"] = from_union([lambda x: to_class(SandboxConfigUserPolicy, x), from_none], self.user_policy) return result @@ -30834,6 +31602,64 @@ def to_dict(self) -> dict: result["error"] = from_union([lambda x: to_class(SessionFSSqliteTransactionError, x), from_none], self.error) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksRegisterResult: + """Result of registering or reclaiming a client-owned task.""" + + created: bool + """True only when this invocation created a new task""" + + reclaimed: bool + """True only when this invocation reclaimed an orphaned task""" + + task: TaskClientInfo + """Authoritative registered or reclaimed task""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksRegisterResult': + assert isinstance(obj, dict) + created = from_bool(obj.get("created")) + reclaimed = from_bool(obj.get("reclaimed")) + task = TaskClientInfo.from_dict(obj.get("task")) + return TasksRegisterResult(created, reclaimed, task) + + def to_dict(self) -> dict: + result: dict = {} + result["created"] = from_bool(self.created) + result["reclaimed"] = from_bool(self.reclaimed) + result["task"] = to_class(TaskClientInfo, self.task) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksUpdateResult: + """Result of publishing a client-owned task update.""" + + applied: bool + """Whether this invocation changed task state""" + + duplicate: bool + """Whether this invocation repeated the latest accepted update""" + + task: TaskClientInfo + """Authoritative task after processing the update""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksUpdateResult': + assert isinstance(obj, dict) + applied = from_bool(obj.get("applied")) + duplicate = from_bool(obj.get("duplicate")) + task = TaskClientInfo.from_dict(obj.get("task")) + return TasksUpdateResult(applied, duplicate, task) + + def to_dict(self) -> dict: + result: dict = {} + result["applied"] = from_bool(self.applied) + result["duplicate"] = from_bool(self.duplicate) + result["task"] = to_class(TaskClientInfo, self.task) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPConfigAddRequest: @@ -31229,6 +32055,9 @@ class SessionOpenOptions: ask_user_disabled: bool | None = None """Whether ask_user is explicitly disabled.""" + auth_client_id_metadata_url: str | None = None + """OAuth Client ID Metadata Document URL used by this host for MCP authorization.""" + auth_info: AuthInfo | None = None """Initial authentication info for the session.""" @@ -31481,6 +32310,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': 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_client_id_metadata_url = from_union([from_str, from_none], obj.get("authClientIdMetadataUrl")) 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")) capi = from_union([CapiSessionOptions.from_dict, from_none], obj.get("capi")) @@ -31547,7 +32377,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': verbosity = from_union([Verbosity, from_none], obj.get("verbosity")) 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, additional_directories, 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_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_skills, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, has_skill_provider, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, 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, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) + return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_client_id_metadata_url, 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_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_skills, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, has_skill_provider, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, 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, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) def to_dict(self) -> dict: result: dict = {} @@ -31561,6 +32391,8 @@ def to_dict(self) -> dict: 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_client_id_metadata_url is not None: + result["authClientIdMetadataUrl"] = from_union([from_str, from_none], self.auth_client_id_metadata_url) if self.auth_info is not None: result["authInfo"] = from_union([lambda x: (x).to_dict(), from_none], self.auth_info) if self.available_tools is not None: @@ -34294,6 +35126,11 @@ class Model: a recommended alternative. Present only when the service published at least one notice. Hosts should surface these without implying anything is wrong with the model. """ + metadata: dict[str, Any] | None = None + """Provider-supplied model metadata. Keys and JSON-compatible values are preserved + unchanged. This is factual metadata published by the model provider; it carries no picker + or UX semantics. + """ model_picker_category: ModelPickerCategory | None = None """Model capability category for grouping in the model picker""" @@ -34331,6 +35168,7 @@ def from_dict(obj: Any) -> 'Model': billing = from_union([ModelBilling.from_dict, from_none], obj.get("billing")) default_reasoning_effort = from_union([from_str, from_none], obj.get("defaultReasoningEffort")) info_messages = from_union([lambda x: from_list(ModelMessage.from_dict, x), from_none], obj.get("infoMessages")) + metadata = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("metadata")) model_picker_category = from_union([ModelPickerCategory, from_none], obj.get("modelPickerCategory")) model_picker_price_category = from_union([ModelPickerPriceCategory, from_none], obj.get("modelPickerPriceCategory")) policy = from_union([ModelPolicy.from_dict, from_none], obj.get("policy")) @@ -34338,7 +35176,7 @@ def from_dict(obj: Any) -> 'Model': supported_reasoning_efforts = from_union([lambda x: from_list(from_str, x), from_none], obj.get("supportedReasoningEfforts")) warning_messages = from_union([lambda x: from_list(ModelMessage.from_dict, x), from_none], obj.get("warningMessages")) warning_text = from_union([ModelWarningText.from_dict, from_none], obj.get("warningText")) - return Model(capabilities, id, name, billing, default_reasoning_effort, info_messages, model_picker_category, model_picker_price_category, policy, supported_context_tiers, supported_reasoning_efforts, warning_messages, warning_text) + return Model(capabilities, id, name, billing, default_reasoning_effort, info_messages, metadata, model_picker_category, model_picker_price_category, policy, supported_context_tiers, supported_reasoning_efforts, warning_messages, warning_text) def to_dict(self) -> dict: result: dict = {} @@ -34351,6 +35189,8 @@ def to_dict(self) -> dict: result["defaultReasoningEffort"] = from_union([from_str, from_none], self.default_reasoning_effort) if self.info_messages is not None: result["infoMessages"] = from_union([lambda x: from_list(lambda x: to_class(ModelMessage, x), x), from_none], self.info_messages) + if self.metadata is not None: + result["metadata"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.metadata) if self.model_picker_category is not None: result["modelPickerCategory"] = from_union([lambda x: to_enum(ModelPickerCategory, x), from_none], self.model_picker_category) if self.model_picker_price_category is not None: @@ -34462,6 +35302,11 @@ class ModelSwitchToRequest: `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. """ + auto_tier: AutoTier | None = None + """Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to + return to provider-default Auto routing. This field is rejected when `modelId` is not + `auto`. + """ compaction_decision: str | None = None """Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. @@ -34514,6 +35359,7 @@ class ModelSwitchToRequest: def from_dict(obj: Any) -> 'ModelSwitchToRequest': assert isinstance(obj, dict) model_id = from_str(obj.get("modelId")) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) compaction_decision = from_union([from_str, from_none], obj.get("compactionDecision")) context_tier = from_union([ContextTier, from_none], obj.get("contextTier")) defer_if_model_change_queued = from_union([from_bool, from_none], obj.get("deferIfModelChangeQueued")) @@ -34527,11 +35373,13 @@ def from_dict(obj: Any) -> 'ModelSwitchToRequest': run_compaction_preflight = from_union([from_bool, from_none], obj.get("runCompactionPreflight")) source = from_union([ModelChangeSource, from_none], obj.get("source")) verbosity = from_union([Verbosity, from_none], obj.get("verbosity")) - return ModelSwitchToRequest(model_id, compaction_decision, context_tier, defer_if_model_change_queued, model_capabilities, model_change_scope, picker_persistence, reasoning_effort, reasoning_summary, repo_scope, require_available, run_compaction_preflight, source, verbosity) + return ModelSwitchToRequest(model_id, auto_tier, compaction_decision, context_tier, defer_if_model_change_queued, model_capabilities, model_change_scope, picker_persistence, reasoning_effort, reasoning_summary, repo_scope, require_available, run_compaction_preflight, source, verbosity) def to_dict(self) -> dict: result: dict = {} result["modelId"] = from_str(self.model_id) + if self.auto_tier is not None: + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) if self.compaction_decision is not None: result["compactionDecision"] = from_union([from_str, from_none], self.compaction_decision) if self.context_tier is not None: @@ -35613,6 +36461,9 @@ class RPC: catalog_unsafe_retrieval_error: CatalogUnsafeRetrievalError catalog_unsafe_retrieval_reason: CatalogUnsafeRetrievalReason catalog_unsupported_kind_error: CatalogUnsupportedKindError + client_task_cancel_reason: ClientTaskCancelReason + client_task_cancel_request: ClientTaskCancelRequest + client_task_cancel_result: ClientTaskCancelResult command_list: CommandList commands_finalize_invocation_effect_request: CommandsFinalizeInvocationEffectRequest commands_finalize_invocation_effect_result: CommandsFinalizeInvocationEffectResult @@ -36032,6 +36883,9 @@ class RPC: model_set_reasoning_effort_request: ModelSetReasoningEffortRequest model_set_reasoning_effort_result: ModelSetReasoningEffortResult models_list_request: ModelsListRequest + model_switch_auto_tier_request: ModelSwitchAutoTierRequest + model_switch_auto_tier_result: ModelSwitchAutoTierResult + model_switch_auto_tier_status: ModelSwitchAutoTierStatus model_switch_confirmation: ModelSwitchConfirmation model_switch_to_request: ModelSwitchToRequest model_switch_to_result: ModelSwitchToResult @@ -36542,10 +37396,21 @@ class RPC: subagent_settings_entry_context_tier: SubagentSettingsEntryContextTier task_agent_info: TaskAgentInfo task_agent_progress: TaskAgentProgress + task_client_active_status: TaskClientActiveStatus + task_client_execution_mode: TaskClientExecutionMode + task_client_info: TaskClientInfo + task_client_owner: TaskClientOwner + task_client_owner_kind: TaskClientOwnerKind + task_client_owner_presence: TaskClientOwnerPresence + task_client_progress: TaskClientProgress + task_client_status: TaskClientStatus + task_client_type: TaskClientType + task_client_update: TaskClientUpdate task_complete_data: TaskCompleteData task_completion_decision: TaskCompletionDecision task_execution_mode: TaskExecutionMode task_info: TaskInfo + task_kind: TaskKind task_list: TaskList task_progress_line: TaskProgressLine tasks_cancel_request: TasksCancelRequest @@ -36560,6 +37425,8 @@ class RPC: tasks_promote_to_background_request: TasksPromoteToBackgroundRequest tasks_promote_to_background_result: TasksPromoteToBackgroundResult tasks_refresh_result: TasksRefreshResult + tasks_register_request: TasksRegisterRequest + tasks_register_result: TasksRegisterResult tasks_remove_request: TasksRemoveRequest tasks_remove_result: TasksRemoveResult tasks_send_message_request: TasksSendMessageRequest @@ -36567,6 +37434,8 @@ class RPC: tasks_start_agent_request: TasksStartAgentRequest tasks_start_agent_result: TasksStartAgentResult task_status: TaskStatus + tasks_update_request: TasksUpdateRequest + tasks_update_result: TasksUpdateResult tasks_wait_for_pending_result: TasksWaitForPendingResult telemetry_set_feature_overrides_request: TelemetrySetFeatureOverridesRequest token_auth_info: TokenAuthInfo @@ -36807,6 +37676,9 @@ def from_dict(obj: Any) -> 'RPC': catalog_unsafe_retrieval_error = CatalogUnsafeRetrievalError.from_dict(obj.get("CatalogUnsafeRetrievalError")) catalog_unsafe_retrieval_reason = CatalogUnsafeRetrievalReason(obj.get("CatalogUnsafeRetrievalReason")) catalog_unsupported_kind_error = CatalogUnsupportedKindError.from_dict(obj.get("CatalogUnsupportedKindError")) + client_task_cancel_reason = ClientTaskCancelReason(obj.get("ClientTaskCancelReason")) + client_task_cancel_request = ClientTaskCancelRequest.from_dict(obj.get("ClientTaskCancelRequest")) + client_task_cancel_result = ClientTaskCancelResult.from_dict(obj.get("ClientTaskCancelResult")) command_list = CommandList.from_dict(obj.get("CommandList")) commands_finalize_invocation_effect_request = CommandsFinalizeInvocationEffectRequest.from_dict(obj.get("CommandsFinalizeInvocationEffectRequest")) commands_finalize_invocation_effect_result = CommandsFinalizeInvocationEffectResult.from_dict(obj.get("CommandsFinalizeInvocationEffectResult")) @@ -37226,6 +38098,9 @@ def from_dict(obj: Any) -> 'RPC': model_set_reasoning_effort_request = ModelSetReasoningEffortRequest.from_dict(obj.get("ModelSetReasoningEffortRequest")) model_set_reasoning_effort_result = ModelSetReasoningEffortResult.from_dict(obj.get("ModelSetReasoningEffortResult")) models_list_request = ModelsListRequest.from_dict(obj.get("ModelsListRequest")) + model_switch_auto_tier_request = ModelSwitchAutoTierRequest.from_dict(obj.get("ModelSwitchAutoTierRequest")) + model_switch_auto_tier_result = ModelSwitchAutoTierResult.from_dict(obj.get("ModelSwitchAutoTierResult")) + model_switch_auto_tier_status = ModelSwitchAutoTierStatus(obj.get("ModelSwitchAutoTierStatus")) model_switch_confirmation = ModelSwitchConfirmation.from_dict(obj.get("ModelSwitchConfirmation")) model_switch_to_request = ModelSwitchToRequest.from_dict(obj.get("ModelSwitchToRequest")) model_switch_to_result = ModelSwitchToResult.from_dict(obj.get("ModelSwitchToResult")) @@ -37736,10 +38611,21 @@ def from_dict(obj: Any) -> 'RPC': subagent_settings_entry_context_tier = SubagentSettingsEntryContextTier(obj.get("SubagentSettingsEntryContextTier")) task_agent_info = TaskAgentInfo.from_dict(obj.get("TaskAgentInfo")) task_agent_progress = TaskAgentProgress.from_dict(obj.get("TaskAgentProgress")) + task_client_active_status = TaskClientActiveStatus(obj.get("TaskClientActiveStatus")) + task_client_execution_mode = TaskClientExecutionMode(obj.get("TaskClientExecutionMode")) + task_client_info = TaskClientInfo.from_dict(obj.get("TaskClientInfo")) + task_client_owner = TaskClientOwner.from_dict(obj.get("TaskClientOwner")) + task_client_owner_kind = TaskClientOwnerKind(obj.get("TaskClientOwnerKind")) + task_client_owner_presence = TaskClientOwnerPresence(obj.get("TaskClientOwnerPresence")) + task_client_progress = TaskClientProgress.from_dict(obj.get("TaskClientProgress")) + task_client_status = TaskClientStatus(obj.get("TaskClientStatus")) + task_client_type = TaskClientType(obj.get("TaskClientType")) + task_client_update = TaskClientUpdate.from_dict(obj.get("TaskClientUpdate")) task_complete_data = TaskCompleteData.from_dict(obj.get("TaskCompleteData")) task_completion_decision = TaskCompletionDecision.from_dict(obj.get("TaskCompletionDecision")) task_execution_mode = TaskExecutionMode(obj.get("TaskExecutionMode")) task_info = _load_TaskInfo(obj.get("TaskInfo")) + task_kind = TaskKind(obj.get("TaskKind")) task_list = TaskList.from_dict(obj.get("TaskList")) task_progress_line = TaskProgressLine.from_dict(obj.get("TaskProgressLine")) tasks_cancel_request = TasksCancelRequest.from_dict(obj.get("TasksCancelRequest")) @@ -37754,6 +38640,8 @@ def from_dict(obj: Any) -> 'RPC': tasks_promote_to_background_request = TasksPromoteToBackgroundRequest.from_dict(obj.get("TasksPromoteToBackgroundRequest")) tasks_promote_to_background_result = TasksPromoteToBackgroundResult.from_dict(obj.get("TasksPromoteToBackgroundResult")) tasks_refresh_result = TasksRefreshResult.from_dict(obj.get("TasksRefreshResult")) + tasks_register_request = TasksRegisterRequest.from_dict(obj.get("TasksRegisterRequest")) + tasks_register_result = TasksRegisterResult.from_dict(obj.get("TasksRegisterResult")) tasks_remove_request = TasksRemoveRequest.from_dict(obj.get("TasksRemoveRequest")) tasks_remove_result = TasksRemoveResult.from_dict(obj.get("TasksRemoveResult")) tasks_send_message_request = TasksSendMessageRequest.from_dict(obj.get("TasksSendMessageRequest")) @@ -37761,6 +38649,8 @@ def from_dict(obj: Any) -> 'RPC': tasks_start_agent_request = TasksStartAgentRequest.from_dict(obj.get("TasksStartAgentRequest")) tasks_start_agent_result = TasksStartAgentResult.from_dict(obj.get("TasksStartAgentResult")) task_status = TaskStatus(obj.get("TaskStatus")) + tasks_update_request = TasksUpdateRequest.from_dict(obj.get("TasksUpdateRequest")) + tasks_update_result = TasksUpdateResult.from_dict(obj.get("TasksUpdateResult")) tasks_wait_for_pending_result = TasksWaitForPendingResult.from_dict(obj.get("TasksWaitForPendingResult")) telemetry_set_feature_overrides_request = TelemetrySetFeatureOverridesRequest.from_dict(obj.get("TelemetrySetFeatureOverridesRequest")) token_auth_info = TokenAuthInfo.from_dict(obj.get("TokenAuthInfo")) @@ -37874,7 +38764,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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_prune_old_request, sessions_read_persisted_events_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, 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, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, 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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_prune_old_request, sessions_read_persisted_events_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, 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_register_request, tasks_register_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_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -38001,6 +38891,9 @@ def to_dict(self) -> dict: result["CatalogUnsafeRetrievalError"] = to_class(CatalogUnsafeRetrievalError, self.catalog_unsafe_retrieval_error) result["CatalogUnsafeRetrievalReason"] = to_enum(CatalogUnsafeRetrievalReason, self.catalog_unsafe_retrieval_reason) result["CatalogUnsupportedKindError"] = to_class(CatalogUnsupportedKindError, self.catalog_unsupported_kind_error) + result["ClientTaskCancelReason"] = to_enum(ClientTaskCancelReason, self.client_task_cancel_reason) + result["ClientTaskCancelRequest"] = to_class(ClientTaskCancelRequest, self.client_task_cancel_request) + result["ClientTaskCancelResult"] = to_class(ClientTaskCancelResult, self.client_task_cancel_result) result["CommandList"] = to_class(CommandList, self.command_list) result["CommandsFinalizeInvocationEffectRequest"] = to_class(CommandsFinalizeInvocationEffectRequest, self.commands_finalize_invocation_effect_request) result["CommandsFinalizeInvocationEffectResult"] = to_class(CommandsFinalizeInvocationEffectResult, self.commands_finalize_invocation_effect_result) @@ -38420,6 +39313,9 @@ def to_dict(self) -> dict: result["ModelSetReasoningEffortRequest"] = to_class(ModelSetReasoningEffortRequest, self.model_set_reasoning_effort_request) result["ModelSetReasoningEffortResult"] = to_class(ModelSetReasoningEffortResult, self.model_set_reasoning_effort_result) result["ModelsListRequest"] = to_class(ModelsListRequest, self.models_list_request) + result["ModelSwitchAutoTierRequest"] = to_class(ModelSwitchAutoTierRequest, self.model_switch_auto_tier_request) + result["ModelSwitchAutoTierResult"] = to_class(ModelSwitchAutoTierResult, self.model_switch_auto_tier_result) + result["ModelSwitchAutoTierStatus"] = to_enum(ModelSwitchAutoTierStatus, self.model_switch_auto_tier_status) result["ModelSwitchConfirmation"] = to_class(ModelSwitchConfirmation, self.model_switch_confirmation) result["ModelSwitchToRequest"] = to_class(ModelSwitchToRequest, self.model_switch_to_request) result["ModelSwitchToResult"] = to_class(ModelSwitchToResult, self.model_switch_to_result) @@ -38930,10 +39826,21 @@ def to_dict(self) -> dict: result["SubagentSettingsEntryContextTier"] = to_enum(SubagentSettingsEntryContextTier, self.subagent_settings_entry_context_tier) result["TaskAgentInfo"] = to_class(TaskAgentInfo, self.task_agent_info) result["TaskAgentProgress"] = to_class(TaskAgentProgress, self.task_agent_progress) + result["TaskClientActiveStatus"] = to_enum(TaskClientActiveStatus, self.task_client_active_status) + result["TaskClientExecutionMode"] = to_enum(TaskClientExecutionMode, self.task_client_execution_mode) + result["TaskClientInfo"] = to_class(TaskClientInfo, self.task_client_info) + result["TaskClientOwner"] = to_class(TaskClientOwner, self.task_client_owner) + result["TaskClientOwnerKind"] = to_enum(TaskClientOwnerKind, self.task_client_owner_kind) + result["TaskClientOwnerPresence"] = to_enum(TaskClientOwnerPresence, self.task_client_owner_presence) + result["TaskClientProgress"] = to_class(TaskClientProgress, self.task_client_progress) + result["TaskClientStatus"] = to_enum(TaskClientStatus, self.task_client_status) + result["TaskClientType"] = to_enum(TaskClientType, self.task_client_type) + result["TaskClientUpdate"] = to_class(TaskClientUpdate, self.task_client_update) result["TaskCompleteData"] = to_class(TaskCompleteData, self.task_complete_data) result["TaskCompletionDecision"] = to_class(TaskCompletionDecision, self.task_completion_decision) result["TaskExecutionMode"] = to_enum(TaskExecutionMode, self.task_execution_mode) result["TaskInfo"] = (self.task_info).to_dict() + result["TaskKind"] = to_enum(TaskKind, self.task_kind) result["TaskList"] = to_class(TaskList, self.task_list) result["TaskProgressLine"] = to_class(TaskProgressLine, self.task_progress_line) result["TasksCancelRequest"] = to_class(TasksCancelRequest, self.tasks_cancel_request) @@ -38948,6 +39855,8 @@ def to_dict(self) -> dict: result["TasksPromoteToBackgroundRequest"] = to_class(TasksPromoteToBackgroundRequest, self.tasks_promote_to_background_request) result["TasksPromoteToBackgroundResult"] = to_class(TasksPromoteToBackgroundResult, self.tasks_promote_to_background_result) result["TasksRefreshResult"] = to_class(TasksRefreshResult, self.tasks_refresh_result) + result["TasksRegisterRequest"] = to_class(TasksRegisterRequest, self.tasks_register_request) + result["TasksRegisterResult"] = to_class(TasksRegisterResult, self.tasks_register_result) result["TasksRemoveRequest"] = to_class(TasksRemoveRequest, self.tasks_remove_request) result["TasksRemoveResult"] = to_class(TasksRemoveResult, self.tasks_remove_result) result["TasksSendMessageRequest"] = to_class(TasksSendMessageRequest, self.tasks_send_message_request) @@ -38955,6 +39864,8 @@ def to_dict(self) -> dict: result["TasksStartAgentRequest"] = to_class(TasksStartAgentRequest, self.tasks_start_agent_request) result["TasksStartAgentResult"] = to_class(TasksStartAgentResult, self.tasks_start_agent_result) result["TaskStatus"] = to_enum(TaskStatus, self.task_status) + result["TasksUpdateRequest"] = to_class(TasksUpdateRequest, self.tasks_update_request) + result["TasksUpdateResult"] = to_class(TasksUpdateResult, self.tasks_update_result) result["TasksWaitForPendingResult"] = to_class(TasksWaitForPendingResult, self.tasks_wait_for_pending_result) result["TelemetrySetFeatureOverridesRequest"] = to_class(TelemetrySetFeatureOverridesRequest, self.telemetry_set_feature_overrides_request) result["TokenAuthInfo"] = to_class(TokenAuthInfo, self.token_auth_info) @@ -39411,14 +40322,15 @@ def _load_SlashCommandInvocationResult(obj: Any) -> "SlashCommandInvocationResul case "set-plan-model": return SlashCommandSetPlanModelResult.from_dict(obj) case _: raise ValueError(f"Unknown SlashCommandInvocationResult kind: {kind!r}") -# Tracked task union returned by task APIs, containing either an agent task or a shell task. -TaskInfo = TaskAgentInfo | TaskShellInfo +# Tracked task union returned by task APIs, containing an agent, client, or shell task. +TaskInfo = TaskAgentInfo | TaskClientInfo | TaskShellInfo def _load_TaskInfo(obj: Any) -> "TaskInfo": assert isinstance(obj, dict) kind = obj.get("type") match kind: case "agent": return TaskAgentInfo.from_dict(obj) + case "client": return TaskClientInfo.from_dict(obj) case "shell": return TaskShellInfo.from_dict(obj) case _: raise ValueError(f"Unknown TaskInfo type: {kind!r}") @@ -39885,6 +40797,10 @@ async def read(self, *, timeout: float | None = None) -> ManagedSettingsReadResu "Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session.\n\nReturns:\n Validated device-managed settings discovered before a session exists." return ManagedSettingsReadResult.from_dict(await self._client.request("managedSettings.read", {}, **_timeout_kwargs(timeout))) + async def clear_cache(self, *, timeout: float | None = None) -> None: + "Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing \"sync account policy\" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed." + await self._client.request("managedSettings.clearCache", {}, **_timeout_kwargs(timeout)) + # Experimental: this API group is experimental and may change or be removed. class ServerRuntimeApi: @@ -40325,7 +41241,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def get_current(self, *, timeout: float | None = None) -> CurrentModel: - "Gets the currently selected model for the session.\n\nReturns:\n The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume." + "Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn.\n\nReturns:\n The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume." return CurrentModel.from_dict(await self._client.request("session.model.getCurrent", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def switch_to(self, params: ModelSwitchToRequest, *, timeout: float | None = None) -> ModelSwitchToResult: @@ -40334,6 +41250,12 @@ async def switch_to(self, params: ModelSwitchToRequest, *, timeout: float | None params_dict["sessionId"] = self._session_id return ModelSwitchToResult.from_dict(await self._client.request("session.model.switchTo", params_dict, **_timeout_kwargs(timeout))) + async def switch_auto_tier(self, params: ModelSwitchAutoTierRequest, *, timeout: float | None = None) -> ModelSwitchAutoTierResult: + "Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`.\n\nArgs:\n params: An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`.\n\nReturns:\n Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed." + 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 ModelSwitchAutoTierResult.from_dict(await self._client.request("session.model.switchAutoTier", params_dict, **_timeout_kwargs(timeout))) + async def set_reasoning_effort(self, params: ModelSetReasoningEffortRequest, *, timeout: float | None = None) -> ModelSetReasoningEffortResult: "Updates the session's reasoning effort without changing the selected model.\n\nArgs:\n params: Reasoning effort level to apply to the currently selected model.\n\nReturns:\n Update the session's reasoning effort without changing the selected model. Use `switchTo` instead when you also need to change the model. The runtime stores the effort on the session and applies it to subsequent turns." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -40601,6 +41523,18 @@ async def list(self, *, timeout: float | None = None) -> TaskList: "Lists background tasks tracked by the session.\n\nReturns:\n Background tasks currently tracked by the session." return TaskList.from_dict(await self._client.request("session.tasks.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def register(self, params: TasksRegisterRequest, *, timeout: float | None = None) -> TasksRegisterResult: + "Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal.\n\nArgs:\n params: Registers or reclaims a client-owned task.\n\nReturns:\n Result of registering or reclaiming a client-owned task." + 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 TasksRegisterResult.from_dict(await self._client.request("session.tasks.register", params_dict, **_timeout_kwargs(timeout))) + + async def update(self, params: TasksUpdateRequest, *, timeout: float | None = None) -> TasksUpdateResult: + "Publishes generic progress or a terminal outcome for a client-owned task.\n\nArgs:\n params: Updates a client-owned task.\n\nReturns:\n Result of publishing a client-owned task update." + 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 TasksUpdateResult.from_dict(await self._client.request("session.tasks.update", params_dict, **_timeout_kwargs(timeout))) + async def refresh(self, *, timeout: float | None = None) -> TasksRefreshResult: "Refreshes metadata for any detached background shells the runtime knows about.\n\nReturns:\n Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop." return TasksRefreshResult.from_dict(await self._client.request("session.tasks.refresh", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) @@ -42022,6 +42956,12 @@ async def abort(self, params: FactoryAbortRequest) -> FactoryACKResult: "Asks the owning extension connection to abort a running factory cooperatively.\n\nArgs:\n params: Parameters for cooperatively aborting a factory body.\n\nReturns:\n Acknowledgement that a factory request was accepted." pass +# Experimental: this API group is experimental and may change or be removed. +class TasksHandler(Protocol): + async def cancel(self, params: ClientTaskCancelRequest) -> ClientTaskCancelResult: + "Asks the client currently bound to a client-owned session task to confirm that its external work stopped.\n\nArgs:\n params: Runtime-to-owner cancellation request for a client-owned task.\n\nReturns:\n Whether the client authoritatively confirmed its external work stopped." + pass + # Experimental: this API group is experimental and may change or be removed. class SessionFsHandler(Protocol): async def read_file(self, params: SessionFSReadFileRequest) -> SessionFSReadFileResult: @@ -42080,6 +43020,7 @@ async def invoke(self, params: CanvasProviderInvokeActionRequest) -> Any: class ClientSessionApiHandlers: provider_token: ProviderTokenHandler | None = None factory: FactoryHandler | None = None + tasks: TasksHandler | None = None session_fs: SessionFsHandler | None = None canvas: CanvasHandler | None = None @@ -42109,6 +43050,13 @@ async def handle_factory_abort(params: dict) -> dict | None: result = await handler.abort(request) return result.to_dict() client.set_request_handler("factory.abort", handle_factory_abort) + async def handle_tasks_cancel(params: dict) -> dict | None: + request = ClientTaskCancelRequest.from_dict(params) + handler = get_handlers(request.session_id).tasks + if handler is None: raise RuntimeError(f"No tasks handler registered for session: {request.session_id}") + result = await handler.cancel(request) + return result.to_dict() + client.set_request_handler("tasks.cancel", handle_tasks_cancel) async def handle_session_fs_read_file(params: dict) -> dict | None: request = SessionFSReadFileRequest.from_dict(params) handler = get_handlers(request.session_id).session_fs @@ -42477,6 +43425,9 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "Categories", "ClientGlobalApiHandlers", "ClientSessionApiHandlers", + "ClientTaskCancelReason", + "ClientTaskCancelRequest", + "ClientTaskCancelResult", "CommandList", "CommandsApi", "CommandsFinalizeInvocationEffectRequest", @@ -42960,6 +43911,9 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ModelPolicyState", "ModelSetReasoningEffortRequest", "ModelSetReasoningEffortResult", + "ModelSwitchAutoTierRequest", + "ModelSwitchAutoTierResult", + "ModelSwitchAutoTierStatus", "ModelSwitchConfirmation", "ModelSwitchToRequest", "ModelSwitchToResult", @@ -43584,13 +44538,24 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "TaskAgentInfo", "TaskAgentInfoType", "TaskAgentProgress", + "TaskClientActiveStatus", + "TaskClientExecutionMode", + "TaskClientInfo", + "TaskClientOwner", + "TaskClientOwnerKind", + "TaskClientOwnerPresence", + "TaskClientProgress", + "TaskClientStatus", + "TaskClientType", + "TaskClientUpdate", + "TaskClientUpdateKind", "TaskCompleteData", "TaskCompletionDecision", "TaskExecutionMode", "TaskInfo", "TaskInfoExecutionMode", "TaskInfoStatus", - "TaskInfoType", + "TaskKind", "TaskList", "TaskProgress", "TaskProgressLine", @@ -43606,16 +44571,21 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "TasksGetCurrentPromotableResult", "TasksGetProgressRequest", "TasksGetProgressResult", + "TasksHandler", "TasksPromoteCurrentToBackgroundResult", "TasksPromoteToBackgroundRequest", "TasksPromoteToBackgroundResult", "TasksRefreshResult", + "TasksRegisterRequest", + "TasksRegisterResult", "TasksRemoveRequest", "TasksRemoveResult", "TasksSendMessageRequest", "TasksSendMessageResult", "TasksStartAgentRequest", "TasksStartAgentResult", + "TasksUpdateRequest", + "TasksUpdateResult", "TasksWaitForPendingResult", "TelemetryApi", "TelemetrySetFeatureOverridesRequest", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 51e719bcf9..636f63bcda 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -136,6 +136,7 @@ class SessionEventType(Enum): SESSION_INFO = "session.info" SESSION_WARNING = "session.warning" SESSION_MODEL_CHANGE = "session.model_change" + SESSION_AUTO_TIER_SWITCH_FAILED = "session.auto_tier_switch_failed" SESSION_MODE_CHANGED = "session.mode_changed" SESSION_MODE_NOTICE_DELIVERED = "session.mode_notice_delivered" SESSION_SESSION_LIMITS_CHANGED = "session.session_limits_changed" @@ -262,6 +263,8 @@ class SessionEventType(Enum): SESSION_CUSTOM_AGENTS_UPDATED = "session.custom_agents_updated" SESSION_MCP_SERVERS_LOADED = "session.mcp_servers_loaded" SESSION_MCP_SERVER_STATUS_CHANGED = "session.mcp_server_status_changed" + SESSION_MCP_SERVER_REMOVED = "session.mcp_server_removed" + SESSION_MCP_SERVER_NEEDS_RECONNECT = "session.mcp_server_needs_reconnect" MCP_TOOLS_LIST_CHANGED = "mcp.tools.list_changed" MCP_RESOURCES_LIST_CHANGED = "mcp.resources.list_changed" MCP_PROMPTS_LIST_CHANGED = "mcp.prompts.list_changed" @@ -7586,6 +7589,34 @@ def to_dict(self) -> dict: return {} +@dataclass +class SessionAutoTierSwitchFailedData: + "A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume." + reason: AutoTierSwitchFailureReason + requested_auto_tier: AutoTier | None + effective_auto_tier: AutoTier | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionAutoTierSwitchFailedData": + assert isinstance(obj, dict) + reason = parse_enum(AutoTierSwitchFailureReason, obj.get("reason")) + requested_auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("requestedAutoTier")) + effective_auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("effectiveAutoTier")) + return SessionAutoTierSwitchFailedData( + reason=reason, + requested_auto_tier=requested_auto_tier, + effective_auto_tier=effective_auto_tier, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["reason"] = to_enum(AutoTierSwitchFailureReason, self.reason) + result["requestedAutoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.requested_auto_tier) + if self.effective_auto_tier is not None: + result["effectiveAutoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.effective_auto_tier) + return result + + @dataclass class SessionAutopilotObjectiveChangedData: "Autopilot objective state file operation details indicating what changed" @@ -8289,6 +8320,44 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionMcpServerNeedsReconnectData: + "Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established." + server_name: str + + @staticmethod + def from_dict(obj: Any) -> "SessionMcpServerNeedsReconnectData": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + return SessionMcpServerNeedsReconnectData( + server_name=server_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + return result + + +@dataclass +class SessionMcpServerRemovedData: + "Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs." + server_name: str + + @staticmethod + def from_dict(obj: Any) -> "SessionMcpServerRemovedData": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + return SessionMcpServerRemovedData( + server_name=server_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + return result + + @dataclass class SessionMcpServerStatusChangedData: "Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error." @@ -8387,8 +8456,10 @@ def to_dict(self) -> dict: class SessionModelChangeData: "Model change details including previous and new model identifiers" new_model: str + auto_tier: AutoTier | None = None cause: str | None = None context_tier: ContextTier | None = None + previous_auto_tier: AutoTier | None = None previous_model: str | None = None previous_reasoning_effort: str | None = None previous_reasoning_summary: ReasoningSummary | None = None @@ -8402,8 +8473,10 @@ class SessionModelChangeData: def from_dict(obj: Any) -> "SessionModelChangeData": assert isinstance(obj, dict) new_model = from_str(obj.get("newModel")) + auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("autoTier")) cause = from_union([from_none, from_str], obj.get("cause")) context_tier = from_union([from_none, lambda x: parse_enum(ContextTier, x)], obj.get("contextTier")) + previous_auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("previousAutoTier")) previous_model = from_union([from_none, from_str], obj.get("previousModel")) previous_reasoning_effort = from_union([from_none, from_str], obj.get("previousReasoningEffort")) previous_reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("previousReasoningSummary")) @@ -8414,8 +8487,10 @@ def from_dict(obj: Any) -> "SessionModelChangeData": verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("verbosity")) return SessionModelChangeData( new_model=new_model, + auto_tier=auto_tier, cause=cause, context_tier=context_tier, + previous_auto_tier=previous_auto_tier, previous_model=previous_model, previous_reasoning_effort=previous_reasoning_effort, previous_reasoning_summary=previous_reasoning_summary, @@ -8429,10 +8504,14 @@ def from_dict(obj: Any) -> "SessionModelChangeData": def to_dict(self) -> dict: result: dict = {} result["newModel"] = from_str(self.new_model) + if self.auto_tier is not None: + result["autoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.auto_tier) if self.cause is not None: result["cause"] = from_union([from_none, from_str], self.cause) if self.context_tier is not None: result["contextTier"] = from_union([from_none, lambda x: to_enum(ContextTier, x)], self.context_tier) + if self.previous_auto_tier is not None: + result["previousAutoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.previous_auto_tier) if self.previous_model is not None: result["previousModel"] = from_union([from_none, from_str], self.previous_model) if self.previous_reasoning_effort is not None: @@ -12033,6 +12112,18 @@ class AutoTier(Enum): INTELLIGENCE = "intelligence" +class AutoTierSwitchFailureReason(Enum): + "Terminal reason an Auto preference activation failed." + # The candidate model was rejected by model policy. + POLICY_REJECTED = "policy_rejected" + # The Auto routing request failed or returned an unusable response. + REQUEST_FAILED = "request_failed" + # The runtime could not prepare the Auto routing request. + SETUP_FAILED = "setup_failed" + # The provider does not support Auto routing. + UNSUPPORTED = "unsupported" + + class AutopilotObjectiveChangedOperation(Enum): "The type of operation performed on the autopilot objective state file" # Autopilot objective state file was created for a new objective. @@ -12647,7 +12738,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | 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 | SessionAutoTierSwitchFailedData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionMcpServerRemovedData | SessionMcpServerNeedsReconnectData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -12686,6 +12777,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_INFO: data = SessionInfoData.from_dict(data_obj) case SessionEventType.SESSION_WARNING: data = SessionWarningData.from_dict(data_obj) case SessionEventType.SESSION_MODEL_CHANGE: data = SessionModelChangeData.from_dict(data_obj) + case SessionEventType.SESSION_AUTO_TIER_SWITCH_FAILED: data = SessionAutoTierSwitchFailedData.from_dict(data_obj) case SessionEventType.SESSION_MODE_CHANGED: data = SessionModeChangedData.from_dict(data_obj) case SessionEventType.SESSION_MODE_NOTICE_DELIVERED: data = SessionModeNoticeDeliveredData.from_dict(data_obj) case SessionEventType.SESSION_SESSION_LIMITS_CHANGED: data = SessionSessionLimitsChangedData.from_dict(data_obj) @@ -12794,6 +12886,8 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_CUSTOM_AGENTS_UPDATED: data = SessionCustomAgentsUpdatedData.from_dict(data_obj) case SessionEventType.SESSION_MCP_SERVERS_LOADED: data = SessionMcpServersLoadedData.from_dict(data_obj) case SessionEventType.SESSION_MCP_SERVER_STATUS_CHANGED: data = SessionMcpServerStatusChangedData.from_dict(data_obj) + case SessionEventType.SESSION_MCP_SERVER_REMOVED: data = SessionMcpServerRemovedData.from_dict(data_obj) + case SessionEventType.SESSION_MCP_SERVER_NEEDS_RECONNECT: data = SessionMcpServerNeedsReconnectData.from_dict(data_obj) case SessionEventType.MCP_TOOLS_LIST_CHANGED: data = McpToolsListChangedData.from_dict(data_obj) case SessionEventType.MCP_RESOURCES_LIST_CHANGED: data = McpResourcesListChangedData.from_dict(data_obj) case SessionEventType.MCP_PROMPTS_LIST_CHANGED: data = McpPromptsListChangedData.from_dict(data_obj) @@ -12904,6 +12998,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AutoModeSwitchRequestedData", "AutoModeSwitchResponse", "AutoTier", + "AutoTierSwitchFailureReason", "AutopilotObjectiveChangedOperation", "AutopilotObjectiveChangedStatus", "BinaryAssetReference", @@ -13081,6 +13176,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SandboxDecisionData", "ScheduleOrigin", "SessionAutoModeResolvedData", + "SessionAutoTierSwitchFailedData", "SessionAutopilotObjectiveChangedData", "SessionBackgroundTasksChangedData", "SessionBinaryAssetData", @@ -13117,6 +13213,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SessionLimitsExhaustedResponseAction", "SessionManagedSettingsEnforcedData", "SessionManagedSettingsResolvedData", + "SessionMcpServerNeedsReconnectData", + "SessionMcpServerRemovedData", "SessionMcpServerStatusChangedData", "SessionMcpServersLoadedData", "SessionMode", diff --git a/python/e2e/test_mcp_oauth_e2e.py b/python/e2e/test_mcp_oauth_e2e.py index 76f1cbf721..3502ccdf4a 100644 --- a/python/e2e/test_mcp_oauth_e2e.py +++ b/python/e2e/test_mcp_oauth_e2e.py @@ -128,6 +128,7 @@ def on_mcp_auth_request(request, _invocation): on_mcp_auth_request=on_mcp_auth_request, mcp_servers=mcp_servers, ) as session: + await session.rpc.mcp.reload() await _wait_for_mcp_server_status(session, server_name) tools = await session.rpc.mcp.list_tools( @@ -164,13 +165,11 @@ async def test_should_resolve_pending_mcp_oauth_request_with_direct_rpc( ): url, process = await _start_oauth_mcp_server() server_name = "oauth-direct-rpc-mcp" - loop = asyncio.get_running_loop() - observed_request = loop.create_future() + observed_requests = asyncio.Queue() release_handler = asyncio.Event() async def on_mcp_auth_request(request, _invocation): - if not observed_request.done(): - observed_request.set_result(request) + observed_requests.put_nowait(request) await release_handler.wait() return {"kind": "token", "accessToken": EXPECTED_TOKEN} @@ -190,9 +189,29 @@ async def on_mcp_auth_request(request, _invocation): mcp_servers=mcp_servers, enable_mcp_apps=True, ) as session: + # session.create can begin MCP startup before the SDK registers OAuth + # event interest. Reload after registration so this test cannot lose + # the initial challenge to that race. + reload_task = asyncio.create_task(session.rpc.mcp.reload()) connected = asyncio.create_task(_wait_for_mcp_server_status(session, server_name)) try: - request = await asyncio.wait_for(observed_request, timeout=30.0) + request = await asyncio.wait_for(observed_requests.get(), timeout=30.0) + while True: + handled = await session.rpc.mcp.oauth.handle_pending_request( + MCPOauthHandlePendingRequest( + request_id=request["requestId"], + result=MCPOauthPendingRequestResponse( + kind=GitHubTokenAcquireResultKind.TOKEN, + access_token=EXPECTED_TOKEN, + token_type="Bearer", + expires_in=3600, + ), + ) + ) + if handled.success: + break + request = await asyncio.wait_for(observed_requests.get(), timeout=30.0) + assert request["serverName"] == server_name assert request["serverUrl"] == f"{url}/mcp" assert request["reason"] == "initial" @@ -202,19 +221,8 @@ async def on_mcp_auth_request(request, _invocation): "error": "invalid_token", } - handled = await session.rpc.mcp.oauth.handle_pending_request( - MCPOauthHandlePendingRequest( - request_id=request["requestId"], - result=MCPOauthPendingRequestResponse( - kind=GitHubTokenAcquireResultKind.TOKEN, - access_token=EXPECTED_TOKEN, - token_type="Bearer", - expires_in=3600, - ), - ) - ) - assert handled.success is True - + release_handler.set() + await asyncio.wait_for(reload_task, timeout=60.0) connected_result = await asyncio.wait_for(connected, timeout=60.0) assert connected_result is None tools = await session.rpc.mcp.list_tools( @@ -225,6 +233,9 @@ async def on_mcp_auth_request(request, _invocation): release_handler.set() if not connected.done(): connected.cancel() + if not reload_task.done(): + reload_task.cancel() + await asyncio.gather(connected, reload_task, return_exceptions=True) finally: await _stop_process(process) @@ -270,7 +281,11 @@ def on_mcp_auth_request(request, _invocation): mcp_servers=mcp_servers, enable_mcp_apps=True, ) as session: + # Re-run startup after OAuth event interest is registered to avoid + # racing the initial challenge emitted during session.create. + await session.rpc.mcp.reload() await _wait_for_mcp_server_status(session, server_name) + refresh_count = 0 for scenario in ("refresh", "upscope", "reauth"): result = await session.rpc.mcp.apps.call_tool( @@ -283,8 +298,9 @@ def on_mcp_auth_request(request, _invocation): ) assert result["content"] == [{"type": "text", "text": "oauth-test-user"}] - assert [request["reason"] for request in observed_requests] == [ - "initial", + assert [ + request["reason"] for request in observed_requests if request["reason"] != "initial" + ] == [ "refresh", "upscope", "refresh", @@ -324,6 +340,7 @@ def on_mcp_auth_request(request, _invocation): on_mcp_auth_request=on_mcp_auth_request, mcp_servers=mcp_servers, ) as session: + await session.rpc.mcp.reload() await _wait_for_mcp_server_status(session, server_name, McpServerStatus.NEEDS_AUTH) # The MCP connection is kicked off by session.create, but the SDK only registers diff --git a/python/test_client.py b/python/test_client.py index b9c74294f3..e62154e247 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -3178,6 +3178,7 @@ async def request(self, method, params, **kwargs): client._client = _FakeClient() await client._verify_protocol_version() assert "enableGitHubTelemetryForwarding" not in captured["connect"] + assert captured["connect"]["supportedTaskKinds"] == ["agent", "client", "shell"] @pytest.mark.asyncio async def test_connect_forwards_client_info(self): diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 68d8b7d054..f1568a8232 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -124,6 +124,8 @@ pub mod rpc_methods { pub const USER_SETTINGS_SET: &str = "user.settings.set"; /// `managedSettings.read` pub const MANAGEDSETTINGS_READ: &str = "managedSettings.read"; + /// `managedSettings.clearCache` + pub const MANAGEDSETTINGS_CLEARCACHE: &str = "managedSettings.clearCache"; /// `runtime.shutdown` pub const RUNTIME_SHUTDOWN: &str = "runtime.shutdown"; /// `sessionFs.setProvider` @@ -286,6 +288,8 @@ pub mod rpc_methods { pub const SESSION_MODEL_GETCURRENT: &str = "session.model.getCurrent"; /// `session.model.switchTo` pub const SESSION_MODEL_SWITCHTO: &str = "session.model.switchTo"; + /// `session.model.switchAutoTier` + pub const SESSION_MODEL_SWITCHAUTOTIER: &str = "session.model.switchAutoTier"; /// `session.model.applyStartupOverlay` pub const SESSION_MODEL_APPLYSTARTUPOVERLAY: &str = "session.model.applyStartupOverlay"; /// `session.model.setReasoningEffort` @@ -374,6 +378,10 @@ pub mod rpc_methods { pub const SESSION_TASKS_STARTAGENT: &str = "session.tasks.startAgent"; /// `session.tasks.list` pub const SESSION_TASKS_LIST: &str = "session.tasks.list"; + /// `session.tasks.register` + pub const SESSION_TASKS_REGISTER: &str = "session.tasks.register"; + /// `session.tasks.update` + pub const SESSION_TASKS_UPDATE: &str = "session.tasks.update"; /// `session.tasks.refresh` pub const SESSION_TASKS_REFRESH: &str = "session.tasks.refresh"; /// `session.tasks.waitForPending` @@ -751,6 +759,8 @@ pub mod rpc_methods { pub const FACTORY_EXECUTE: &str = "factory.execute"; /// `factory.abort` pub const FACTORY_ABORT: &str = "factory.abort"; + /// `tasks.cancel` + pub const TASKS_CANCEL: &str = "tasks.cancel"; /// `sessionFs.readFile` pub const SESSIONFS_READFILE: &str = "sessionFs.readFile"; /// `sessionFs.writeFile` @@ -3036,7 +3046,7 @@ pub struct CanvasProviderUnregisterRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CapiSessionOptions { - /// Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. + /// Routing preference for sessions whose model is `auto`. On create or cold resume, this establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold resume, the runtime restores the last committed preference. On resident resume, a different value requests a safe switch after resume succeeds and cannot change an in-flight turn. Successful switches are persisted for later cold resume. When no preference is supplied or restored, CAPI default routing is used. #[serde(skip_serializing_if = "Option::is_none")] pub auto_tier: Option, /// Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. @@ -3549,6 +3559,44 @@ pub struct CatalogUnavailableTransportError { pub reason: CatalogUnavailableTransportReason, } +/// Runtime-to-owner cancellation request for a client-owned task. +/// +///

+/// +/// **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 ClientTaskCancelRequest { + /// Opaque identifier shared by coalesced cancellation callers + pub cancellation_id: String, + /// Owner-scoped task key included for correlation + pub client_task_id: String, + /// Canonical runtime-generated task identifier + pub id: String, + /// Reason the runtime requests cancellation + pub reason: ClientTaskCancelReason, + /// Session that owns the client task + pub session_id: SessionId, +} + +/// Whether the client authoritatively confirmed its external work stopped. +/// +///
+/// +/// **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 ClientTaskCancelResult { + /// True only when the owner confirms that external work stopped before responding + pub cancelled: bool, +} + /// A literal choice the command input accepts, with a human-facing description /// ///
@@ -3995,6 +4043,9 @@ pub(crate) struct ConnectRequest { /// Opt this connection in to GitHub telemetry forwarding for its lifetime. When set, the runtime forwards every internal telemetry event it emits — across all sessions, plus sessionless events — to this connection over the `gitHubTelemetry.event` notification. Regular events are also written to the runtime's normal GitHub/CTS path (dual-write); host-only compatibility events are forward-only and intentionally skip that path. Intended for first-party hosts that re-emit the events into their own telemetry stores. Both unrestricted and restricted events are forwarded, each tagged with a `restricted` discriminator; a backstop drops restricted events when restricted telemetry is disabled — using the process-global gate for ordinary events and an explicit session-scoped decision for host-only events. #[serde(skip_serializing_if = "Option::is_none")] pub enable_git_hub_telemetry_forwarding: Option, + /// Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_task_kinds: Option>, /// Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, @@ -4015,6 +4066,9 @@ pub(crate) struct ConnectResult { pub ok: bool, /// Server protocol version number pub protocol_version: i64, + /// Task kinds the server may return to this connection. + #[serde(skip_serializing_if = "Option::is_none")] + pub task_kinds: Option>, /// Server package version pub version: String, } @@ -4089,7 +4143,7 @@ pub struct ContextHeaviestMessage { pub tokens: i64, } -/// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. +/// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. /// ///
/// @@ -4100,12 +4154,21 @@ pub struct ContextHeaviestMessage { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CurrentModel { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub activating_auto_tier: Option, + /// Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Context tier for models that support multiple context-window sizes. #[serde(skip_serializing_if = "Option::is_none")] pub context_tier: Option, /// Currently active model identifier #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_auto_tier: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -7718,6 +7781,9 @@ pub struct McpConfigList { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpConfigRemoveRequest { + /// OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_client_id_metadata_url: Option, /// Name of the MCP server to remove pub name: String, } @@ -10257,6 +10323,9 @@ pub struct Model { /// Informational notices the service published for this model, such as an upcoming change or a recommended alternative. Present only when the service published at least one notice. Hosts should surface these without implying anything is wrong with the model. #[serde(skip_serializing_if = "Option::is_none")] pub info_messages: Option>, + /// Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option>, /// Model capability category for grouping in the model picker #[serde(skip_serializing_if = "Option::is_none")] pub model_picker_category: Option, @@ -10536,6 +10605,51 @@ pub struct ModelsListRequest { pub selection_id: Option, } +/// An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. +/// +///
+/// +/// **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 ModelSwitchAutoTierRequest { + /// Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. + pub auto_tier: Option, + /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +/// +///
+/// +/// **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 ModelSwitchAutoTierResult { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub activating_auto_tier: Option, + /// Auto preference currently committed for the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub effective_auto_tier: Option, + /// Latest unclaimed Auto preference waiting for a future user turn. + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_auto_tier: Option, + /// Immediate request status. `pending` means accepted but not committed. + pub status: ModelSwitchAutoTierStatus, + /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + #[serde(skip_serializing_if = "Option::is_none")] + pub superseded_auto_tier: Option, +} + /// ///
/// @@ -10565,6 +10679,9 @@ pub struct ModelSwitchConfirmation { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModelSwitchToRequest { + /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. #[serde(skip_serializing_if = "Option::is_none")] pub compaction_decision: Option, @@ -10634,6 +10751,9 @@ pub struct ModelSwitchToResult { /// Currently active model identifier after the switch #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_state: Option, /// Persistence failure encountered after applying the model switch. #[serde(skip_serializing_if = "Option::is_none")] pub persistence_error: Option, @@ -14848,6 +14968,9 @@ pub struct SandboxConfig { /// Whether to auto-add the current working directory to readwritePaths. Default: true. #[serde(skip_serializing_if = "Option::is_none")] pub add_current_working_directory: Option, + /// Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_bypass: Option, /// Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). #[serde(skip_serializing_if = "Option::is_none")] pub allow_dev_tool_access: Option, @@ -14856,6 +14979,20 @@ pub struct SandboxConfig { pub auth: Option, /// Whether sandboxing is enabled for the session. pub enabled: bool, + /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) managed_lsp_routing_locked: Option, + /// Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) managed_mcp_routing_locked: Option, + /// Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_lsp_servers: Option, + /// Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_mcp_servers: Option, /// User-managed sandbox policy fragment merged into the auto-discovered base policy. #[serde(skip_serializing_if = "Option::is_none")] pub user_policy: Option, @@ -16815,6 +16952,9 @@ pub struct SessionOpenOptions { /// Whether ask_user is explicitly disabled. #[serde(skip_serializing_if = "Option::is_none")] pub ask_user_disabled: Option, + /// OAuth Client ID Metadata Document URL used by this host for MCP authorization. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_client_id_metadata_url: Option, /// Initial authentication info for the session. #[serde(skip_serializing_if = "Option::is_none")] pub auth_info: Option, @@ -19361,6 +19501,199 @@ pub struct TaskAgentProgress { pub r#type: TaskAgentProgressType, } +/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. +/// +///
+/// +/// **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 TaskClientOwner { + /// ISO 8601 timestamp when the bound join disconnected + #[serde(skip_serializing_if = "Option::is_none")] + pub disconnected_at: Option, + /// Display-only owner name + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Opaque identity of the currently or most recently bound session join + pub join_id: String, + /// Class of the task owner + pub kind: TaskClientOwnerKind, + /// Opaque session-scoped participant identity + pub participant_id: String, + /// Whether this task's bound join is currently connected + pub presence: TaskClientOwnerPresence, + /// Display-only owner source + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +/// Tracked client-owned task metadata. +/// +///
+/// +/// **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 TaskClientInfo { + /// ISO 8601 timestamp when the current active segment started + #[serde(skip_serializing_if = "Option::is_none")] + pub active_started_at: Option, + /// Accumulated active execution time in milliseconds + pub active_time_ms: i64, + /// Whether the currently bound owner can receive a cancellation request + pub can_cancel: bool, + /// Human-readable reason for terminal cancellation + #[serde(skip_serializing_if = "Option::is_none")] + pub cancellation_reason: Option, + /// Owner-scoped registration and reclaim key + pub client_task_id: String, + /// ISO 8601 timestamp when the task reached a terminal status + #[serde(skip_serializing_if = "Option::is_none")] + pub completed_at: Option, + /// Task description + pub description: String, + /// Optional task display name + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Human-readable terminal failure message + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Optional owner-supplied terminal failure code + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, + /// Execution mode, which is always background for client-owned tasks + pub execution_mode: TaskClientExecutionMode, + /// Canonical runtime-generated task identifier + pub id: String, + /// ISO 8601 timestamp when the connected owner entered idle status + #[serde(skip_serializing_if = "Option::is_none")] + pub idle_since: Option, + /// ISO 8601 timestamp of the most recent orphan transition + #[serde(skip_serializing_if = "Option::is_none")] + pub orphaned_at: Option, + /// Public attribution and presence for the task owner + pub owner: TaskClientOwner, + /// ISO 8601 timestamp of the most recent successful reclaim + #[serde(skip_serializing_if = "Option::is_none")] + pub reclaimed_at: Option, + /// Opaque successful terminal result supplied by the task owner + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Sequence number of the latest accepted owner update + pub sequence: i64, + /// ISO 8601 timestamp when the task started + pub started_at: String, + /// Client task lifecycle status + pub status: TaskClientStatus, + /// Task kind + pub r#type: TaskClientType, + /// ISO 8601 timestamp of the latest accepted lifecycle change + pub updated_at: String, +} + +/// Generic progress for a client-owned task. +/// +///
+/// +/// **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 TaskClientProgress { + /// Most recent nonempty progress message + #[serde(skip_serializing_if = "Option::is_none")] + pub last_message: Option, + /// Current completion percentage from zero through one hundred + #[serde(skip_serializing_if = "Option::is_none")] + pub percentage: Option, + /// Current owner-defined progress phase + #[serde(skip_serializing_if = "Option::is_none")] + pub phase: Option, + /// Recent server-timestamped progress messages + pub recent_activity: Vec, + /// Sequence number of the latest accepted owner update + pub sequence: i64, + /// Current client task lifecycle status + pub status: TaskClientStatus, + /// Progress kind + pub r#type: TaskClientType, + /// ISO 8601 timestamp of the latest accepted lifecycle change + pub updated_at: String, +} + +/// Publishes nonterminal progress for a running or idle client task. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientUpdateProgress { + /// Client task update variant discriminator. + pub kind: TaskClientUpdateProgressKind, + /// Optional progress message appended to recent activity when nonempty + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Optional completion percentage; null clears the current percentage + #[serde(skip_serializing_if = "Option::is_none")] + pub percentage: Option, + /// Optional progress phase; null clears the current phase + #[serde(skip_serializing_if = "Option::is_none")] + pub phase: Option, + /// Optional active status transition + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +/// Reports successful terminal completion. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientUpdateCompleted { + /// Client task update variant discriminator. + pub kind: TaskClientUpdateCompletedKind, + /// Optional final progress message + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Optional opaque successful terminal result + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, +} + +/// Reports terminal failure. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientUpdateFailed { + /// Optional owner-supplied terminal failure code + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + /// Human-readable terminal failure message + pub error: String, + /// Client task update variant discriminator. + pub kind: TaskClientUpdateFailedKind, + /// Optional final progress message + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +/// Reports terminal cancellation after external work stopped. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientUpdateCancelled { + /// Client task update variant discriminator. + pub kind: TaskClientUpdateCancelledKind, + /// Optional final progress message + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Optional human-readable cancellation reason + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + /// Task completion notification with summary from the agent /// ///
@@ -19509,7 +19842,7 @@ pub struct TasksGetProgressRequest { #[serde(rename_all = "camelCase")] pub struct TasksGetProgressResult { /// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. - pub progress: Option, + pub progress: serde_json::Value, } /// Tracked shell task metadata, including ID, command, status, timing, attachment/execution mode, log path, and PID. @@ -19632,6 +19965,52 @@ pub struct TasksPromoteToBackgroundResult { #[serde(rename_all = "camelCase")] pub struct TasksRefreshResult {} +/// Registers or reclaims a client-owned task. +/// +///
+/// +/// **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 TasksRegisterRequest { + /// Whether the owner supports runtime cancellation requests + pub cancellable: bool, + /// Owner-scoped idempotency key used for registration and reclaim + pub client_task_id: String, + /// Human-readable description of the external work + pub description: String, + /// Optional short display name for the external work + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Expected current sequence for idempotent registration or orphan reclaim + #[serde(skip_serializing_if = "Option::is_none")] + pub expected_sequence: Option, + /// Task kind + pub r#type: TaskClientType, +} + +/// Result of registering or reclaiming a client-owned task. +/// +///
+/// +/// **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 TasksRegisterResult { + /// True only when this invocation created a new task + pub created: bool, + /// True only when this invocation reclaimed an orphaned task + pub reclaimed: bool, + /// Authoritative registered or reclaimed task + pub task: TaskClientInfo, +} + /// Identifier of the completed or cancelled task to remove from tracking. /// ///
@@ -19740,6 +20119,44 @@ pub struct TasksStartAgentResult { pub agent_id: String, } +/// Updates a client-owned task. +/// +///
+/// +/// **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 TasksUpdateRequest { + /// Canonical runtime-generated task identifier + pub id: String, + /// Owner update sequence to apply + pub sequence: i64, + /// Progress or terminal update payload + pub update: TaskClientUpdate, +} + +/// Result of publishing a client-owned task update. +/// +///
+/// +/// **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 TasksUpdateResult { + /// Whether this invocation changed task state + pub applied: bool, + /// Whether this invocation repeated the latest accepted update + pub duplicate: bool, + /// Authoritative task after processing the update + pub task: TaskClientInfo, +} + /// Wait until all in-flight background tasks (agents + shells) and any follow-up turns scheduled by their completions have settled. Returns when the runtime is fully drained or after an internal timeout (default 10 minutes; configurable via COPILOT_TASK_WAIT_TIMEOUT_SECONDS). /// ///
@@ -22970,7 +23387,7 @@ pub struct SessionModelGetCurrentParams { pub session_id: SessionId, } -/// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. +/// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. /// ///
/// @@ -22981,12 +23398,21 @@ pub struct SessionModelGetCurrentParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionModelGetCurrentResult { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub activating_auto_tier: Option, + /// Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Context tier for models that support multiple context-window sizes. #[serde(skip_serializing_if = "Option::is_none")] pub context_tier: Option, /// Currently active model identifier #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_auto_tier: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -23018,6 +23444,9 @@ pub struct SessionModelSwitchToResult { /// Currently active model identifier after the switch #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_state: Option, /// Persistence failure encountered after applying the model switch. #[serde(skip_serializing_if = "Option::is_none")] pub persistence_error: Option, @@ -23029,6 +23458,33 @@ pub struct SessionModelSwitchToResult { pub warning: Option, } +/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +/// +///
+/// +/// **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 SessionModelSwitchAutoTierResult { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub activating_auto_tier: Option, + /// Auto preference currently committed for the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub effective_auto_tier: Option, + /// Latest unclaimed Auto preference waiting for a future user turn. + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_auto_tier: Option, + /// Immediate request status. `pending` means accepted but not committed. + pub status: ModelSwitchAutoTierStatus, + /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + #[serde(skip_serializing_if = "Option::is_none")] + pub superseded_auto_tier: Option, +} + /// The model identifier active on the session after the switch. /// ///
@@ -23055,6 +23511,9 @@ pub struct SessionModelApplyStartupOverlayResult { /// Currently active model identifier after the switch #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_state: Option, /// Persistence failure encountered after applying the model switch. #[serde(skip_serializing_if = "Option::is_none")] pub persistence_error: Option, @@ -24134,6 +24593,44 @@ pub struct SessionTasksListResult { pub tasks: Vec, } +/// Result of registering or reclaiming a client-owned task. +/// +///
+/// +/// **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 SessionTasksRegisterResult { + /// True only when this invocation created a new task + pub created: bool, + /// True only when this invocation reclaimed an orphaned task + pub reclaimed: bool, + /// Authoritative registered or reclaimed task + pub task: TaskClientInfo, +} + +/// Result of publishing a client-owned task update. +/// +///
+/// +/// **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 SessionTasksUpdateResult { + /// Whether this invocation changed task state + pub applied: bool, + /// Whether this invocation repeated the latest accepted update + pub duplicate: bool, + /// Authoritative task after processing the update + pub task: TaskClientInfo, +} + /// Identifies the target session. /// ///
@@ -24200,7 +24697,7 @@ pub struct SessionTasksWaitForPendingResult {} #[serde(rename_all = "camelCase")] pub struct SessionTasksGetProgressResult { /// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. - pub progress: Option, + pub progress: serde_json::Value, } /// Identifies the target session. @@ -28886,6 +29383,28 @@ pub enum CatalogUnavailableTransportReason { Unknown, } +/// Why the runtime requests client-task cancellation. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum ClientTaskCancelReason { + /// A caller requested task cancellation. + #[serde(rename = "cancel_requested")] + CancelRequested, + /// The session is shutting down. + #[serde(rename = "session_shutdown")] + SessionShutdown, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Optional completion hint for the input (e.g. 'directory' for filesystem path completion) /// ///
@@ -28991,6 +29510,31 @@ pub enum ConnectedRemoteSessionMetadataKind { Unknown, } +/// Closed set of public task kinds a connection can negotiate. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskKind { + /// Runtime-owned background agent task. + #[serde(rename = "agent")] + Agent, + /// Runtime-owned shell task. + #[serde(rename = "shell")] + Shell, + /// Client-owned externally executed task. + #[serde(rename = "client")] + Client, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Controls how MCP tool result content is filtered: none leaves content unchanged, markdown sanitizes HTML while preserving Markdown-friendly output, and hidden_characters removes characters that can hide directives. /// ///
@@ -31209,6 +31753,28 @@ pub enum ModelPolicyState { Unknown, } +/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelSwitchAutoTierStatus { + /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. + #[serde(rename = "unchanged")] + Unchanged, + /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. + #[serde(rename = "pending")] + Pending, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Provider transport. Defaults to "http". /// ///
@@ -33572,6 +34138,191 @@ pub enum TaskAgentProgressType { Agent, } +/// Active status a client owner may publish with a progress update. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientActiveStatus { + /// The external owner is actively working. + #[serde(rename = "running")] + Running, + /// The external owner is connected but waiting. + #[serde(rename = "idle")] + Idle, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Client-owned tasks always execute outside the runtime in background mode. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientExecutionMode { + #[serde(rename = "background")] + Background, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Connection class owning a client task. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientOwnerKind { + /// A discovered extension connection owns the task. + #[serde(rename = "extension")] + Extension, + /// A generic SDK connection owns the task. + #[serde(rename = "sdk")] + Sdk, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Presence of the task's bound join. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientOwnerPresence { + /// The bound session join is connected. + #[serde(rename = "connected")] + Connected, + /// The bound session join is disconnected. + #[serde(rename = "disconnected")] + Disconnected, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Lifecycle status of a client-owned task. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientStatus { + /// The external owner is actively working. + #[serde(rename = "running")] + Running, + /// The external owner is connected but waiting. + #[serde(rename = "idle")] + Idle, + /// The owner reported successful completion. + #[serde(rename = "completed")] + Completed, + /// The owner reported failure. + #[serde(rename = "failed")] + Failed, + /// The owner reported or confirmed cancellation. + #[serde(rename = "cancelled")] + Cancelled, + /// The bound owner join disappeared; external executor state is unknown. + #[serde(rename = "orphaned")] + Orphaned, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Discriminator for a client-owned task. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientType { + #[serde(rename = "client")] + Client, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Client task update variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientUpdateProgressKind { + #[serde(rename = "progress")] + #[default] + Progress, +} + +/// Client task update variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientUpdateCompletedKind { + #[serde(rename = "completed")] + #[default] + Completed, +} + +/// Client task update variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientUpdateFailedKind { + #[serde(rename = "failed")] + #[default] + Failed, +} + +/// Client task update variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientUpdateCancelledKind { + #[serde(rename = "cancelled")] + #[default] + Cancelled, +} + +/// Progress or terminal update for a client-owned task. +/// +///
+/// +/// **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 TaskClientUpdate { + Progress(TaskClientUpdateProgress), + Completed(TaskClientUpdateCompleted), + Failed(TaskClientUpdateFailed), + Cancelled(TaskClientUpdateCancelled), +} + /// Whether the shell runs inside a managed PTY session or as an independent background process /// ///
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index d31cea5ce9..9dd8f9e58b 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -905,6 +905,26 @@ impl<'a> ClientRpcManagedSettings<'a> { .await?; Ok(serde_json::from_value(_value)?) } + + /// Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. + /// + /// Wire method: `managedSettings.clearCache`. + /// + ///
+ /// + /// **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 clear_cache(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::MANAGEDSETTINGS_CLEARCACHE, Some(wire_params)) + .await?; + Ok(()) + } } /// `mcp.*` RPCs. @@ -7434,13 +7454,13 @@ pub struct SessionRpcModel<'a> { } impl<'a> SessionRpcModel<'a> { - /// Gets the currently selected model for the session. + /// Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. /// /// Wire method: `session.model.getCurrent`. /// /// # Returns /// - /// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + /// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. /// ///
/// @@ -7492,6 +7512,39 @@ impl<'a> SessionRpcModel<'a> { Ok(serde_json::from_value(_value)?) } + /// Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. + /// + /// Wire method: `session.model.switchAutoTier`. + /// + /// # Parameters + /// + /// * `params` - An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + /// + /// # Returns + /// + /// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + /// + ///
+ /// + /// **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 switch_auto_tier( + &self, + params: ModelSwitchAutoTierRequest, + ) -> 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_MODEL_SWITCHAUTOTIER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Resolves and applies organization-managed and repository model overlays. /// /// Wire method: `session.model.applyStartupOverlay`. @@ -10243,6 +10296,69 @@ impl<'a> SessionRpcTasks<'a> { Ok(serde_json::from_value(_value)?) } + /// Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. + /// + /// Wire method: `session.tasks.register`. + /// + /// # Parameters + /// + /// * `params` - Registers or reclaims a client-owned task. + /// + /// # Returns + /// + /// Result of registering or reclaiming a client-owned task. + /// + ///
+ /// + /// **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 register( + &self, + params: TasksRegisterRequest, + ) -> 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_TASKS_REGISTER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Publishes generic progress or a terminal outcome for a client-owned task. + /// + /// Wire method: `session.tasks.update`. + /// + /// # Parameters + /// + /// * `params` - Updates a client-owned task. + /// + /// # Returns + /// + /// Result of publishing a client-owned task update. + /// + ///
+ /// + /// **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 update(&self, params: TasksUpdateRequest) -> 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_TASKS_UPDATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Refreshes metadata for any detached background shells the runtime knows about. /// /// Wire method: `session.tasks.refresh`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index eb63f11438..fff10bd697 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -37,6 +37,8 @@ pub enum SessionEventType { SessionWarning, #[serde(rename = "session.model_change")] SessionModelChange, + #[serde(rename = "session.auto_tier_switch_failed")] + SessionAutoTierSwitchFailed, #[serde(rename = "session.mode_changed")] SessionModeChanged, #[serde(rename = "session.mode_notice_delivered")] @@ -379,6 +381,10 @@ pub enum SessionEventType { SessionMcpServersLoaded, #[serde(rename = "session.mcp_server_status_changed")] SessionMcpServerStatusChanged, + #[serde(rename = "session.mcp_server_removed")] + SessionMcpServerRemoved, + #[serde(rename = "session.mcp_server_needs_reconnect")] + SessionMcpServerNeedsReconnect, #[serde(rename = "mcp.tools.list_changed")] McpToolsListChanged, #[serde(rename = "mcp.resources.list_changed")] @@ -483,6 +489,8 @@ pub enum SessionEventData { SessionWarning(SessionWarningData), #[serde(rename = "session.model_change")] SessionModelChange(SessionModelChangeData), + #[serde(rename = "session.auto_tier_switch_failed")] + SessionAutoTierSwitchFailed(SessionAutoTierSwitchFailedData), #[serde(rename = "session.mode_changed")] SessionModeChanged(SessionModeChangedData), #[serde(rename = "session.mode_notice_delivered")] @@ -818,6 +826,10 @@ pub enum SessionEventData { SessionMcpServersLoaded(SessionMcpServersLoadedData), #[serde(rename = "session.mcp_server_status_changed")] SessionMcpServerStatusChanged(SessionMcpServerStatusChangedData), + #[serde(rename = "session.mcp_server_removed")] + SessionMcpServerRemoved(SessionMcpServerRemovedData), + #[serde(rename = "session.mcp_server_needs_reconnect")] + SessionMcpServerNeedsReconnect(SessionMcpServerNeedsReconnectData), #[serde(rename = "mcp.tools.list_changed")] McpToolsListChanged(McpToolsListChangedData), #[serde(rename = "mcp.resources.list_changed")] @@ -1231,6 +1243,9 @@ pub struct SessionWarningData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionModelChangeData { + /// Committed Auto preference after the model configuration change, when applicable. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. #[serde(skip_serializing_if = "Option::is_none")] pub cause: Option, @@ -1239,6 +1254,9 @@ pub struct SessionModelChangeData { pub context_tier: Option, /// Newly selected model identifier pub new_model: String, + /// Previously committed Auto preference, when one was explicitly selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub previous_auto_tier: Option, /// Model that was previously selected, if any #[serde(skip_serializing_if = "Option::is_none")] pub previous_model: Option, @@ -1265,6 +1283,19 @@ pub struct SessionModelChangeData { pub verbosity: Option, } +/// Session event "session.auto_tier_switch_failed". A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionAutoTierSwitchFailedData { + /// Auto preference that remains effective after the failed request. + #[serde(skip_serializing_if = "Option::is_none")] + pub effective_auto_tier: Option, + /// Low-cardinality failure outcome reported by Auto resolution. + pub reason: AutoTierSwitchFailureReason, + /// Auto preference that failed to activate, or null when returning to provider-default routing failed. + pub requested_auto_tier: Option, +} + /// Session event "session.mode_changed". Agent mode change details including previous and new modes #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -6238,6 +6269,22 @@ pub struct SessionMcpServerStatusChangedData { pub status: McpServerStatus, } +/// Session event "session.mcp_server_removed". Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpServerRemovedData { + /// Name of the MCP server that was removed from the graph + pub server_name: String, +} + +/// Session event "session.mcp_server_needs_reconnect". Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpServerNeedsReconnectData { + /// Name of the MCP server that needs to reconnect + pub server_name: String, +} + /// Session event "mcp.tools.list_changed". Payload identifying the MCP server associated with a list change. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -6733,6 +6780,27 @@ pub enum ModelChangeSource { Unknown, } +/// Terminal reason an Auto preference activation failed. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AutoTierSwitchFailureReason { + /// The candidate model was rejected by model policy. + #[serde(rename = "policy_rejected")] + PolicyRejected, + /// The Auto routing request failed or returned an unusable response. + #[serde(rename = "request_failed")] + RequestFailed, + /// The runtime could not prepare the Auto routing request. + #[serde(rename = "setup_failed")] + SetupFailed, + /// The provider does not support Auto routing. + #[serde(rename = "unsupported")] + Unsupported, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Permission mode for the session. /// ///
diff --git a/rust/src/lib.rs b/rust/src/lib.rs index fd7f12cf14..0a3886a82e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2477,6 +2477,11 @@ impl Client { .on_github_telemetry .is_some() .then_some(true), + supported_task_kinds: Some(vec![ + crate::generated::api_types::TaskKind::Agent, + crate::generated::api_types::TaskKind::Client, + crate::generated::api_types::TaskKind::Shell, + ]), // Declare the integrating application's identity so the runtime attributes // the telemetry it emits on this connection to a consistent surface // instead of its own build. `None` when the app didn't supply it, and diff --git a/rust/src/session.rs b/rust/src/session.rs index 509485e39e..029e640948 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -549,6 +549,7 @@ impl Session { pub async fn set_model(&self, model: &str, opts: Option) -> Result<(), Error> { let opts = opts.unwrap_or_default(); let request = ModelSwitchToRequest { + auto_tier: None, compaction_decision: None, context_tier: opts.context_tier, defer_if_model_change_queued: None, diff --git a/rust/tests/e2e/mcp_oauth.rs b/rust/tests/e2e/mcp_oauth.rs index fb202536c7..e99e70500c 100644 --- a/rust/tests/e2e/mcp_oauth.rs +++ b/rust/tests/e2e/mcp_oauth.rs @@ -53,6 +53,12 @@ async fn should_satisfy_mcp_oauth_using_host_provided_token() { .await .expect("create session"); + session + .rpc() + .mcp() + .reload() + .await + .expect("reload MCP servers"); wait_for_mcp_server_status(&session, server_name, McpServerStatus::Connected).await; let tools = session .rpc() @@ -142,15 +148,27 @@ async fn should_request_replacement_tokens_across_mcp_oauth_lifecycle() { .await .expect("create session"); + session + .rpc() + .mcp() + .reload() + .await + .expect("reload MCP servers"); 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; + let replacement_reasons = handler + .reasons + .lock() + .iter() + .filter(|reason| **reason != McpOauthRequestReason::Initial) + .cloned() + .collect::>(); assert_eq!( - handler.reasons.lock().as_slice(), + replacement_reasons, [ - McpOauthRequestReason::Initial, McpOauthRequestReason::Refresh, McpOauthRequestReason::Upscope, McpOauthRequestReason::Refresh, @@ -207,15 +225,14 @@ async fn should_cancel_pending_mcp_oauth_request() { .await .expect("create session"); + session + .rpc() + .mcp() + .reload() + .await + .expect("reload MCP servers"); wait_for_mcp_server_status(&session, server_name, McpServerStatus::NeedsAuth).await; - // The MCP connection is kicked off by session.create, but the SDK only registers its - // `mcp.oauth_required` event interest once create returns. If the server's initial 401 - // wins that race, the runtime records `needs-auth` WITHOUT invoking the host callback, - // so `handler.request` is briefly `None` even after `needs-auth` is observed. A later - // auth retry (now that interest is registered) invokes the callback with the same - // `Initial` reason. Wait for the callback rather than sampling it the instant - // `needs-auth` first appears, which is what made this test flaky. wait_for_condition("MCP OAuth request reaching the host callback", || async { handler.request.lock().is_some() }) diff --git a/rust/tests/e2e/rpc_mcp_config.rs b/rust/tests/e2e/rpc_mcp_config.rs index 591d7d247c..29f5f42d39 100644 --- a/rust/tests/e2e/rpc_mcp_config.rs +++ b/rust/tests/e2e/rpc_mcp_config.rs @@ -18,6 +18,7 @@ async fn should_call_server_mcp_config_rpcs() { let _ = config .remove(McpConfigRemoveRequest { name: server_name.to_string(), + auth_client_id_metadata_url: None, }) .await; @@ -74,6 +75,7 @@ async fn should_call_server_mcp_config_rpcs() { config .remove(McpConfigRemoveRequest { name: server_name.to_string(), + auth_client_id_metadata_url: None, }) .await .expect("remove"); @@ -102,6 +104,7 @@ async fn should_round_trip_http_mcp_oauth_config_rpc() { let _ = config .remove(McpConfigRemoveRequest { name: server_name.to_string(), + auth_client_id_metadata_url: None, }) .await; @@ -197,6 +200,7 @@ async fn should_round_trip_http_mcp_oauth_config_rpc() { config .remove(McpConfigRemoveRequest { name: server_name.to_string(), + auth_client_id_metadata_url: None, }) .await .expect("remove"); diff --git a/rust/tests/e2e/rpc_tasks_and_handlers.rs b/rust/tests/e2e/rpc_tasks_and_handlers.rs index 540238ddd0..0b8e7d0642 100644 --- a/rust/tests/e2e/rpc_tasks_and_handlers.rs +++ b/rust/tests/e2e/rpc_tasks_and_handlers.rs @@ -66,7 +66,7 @@ async fn should_list_task_state_and_return_false_for_missing_task_operations() { .await .expect("progress missing") .progress - .is_none() + .is_null() ); assert!( session diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index b4297a239c..f609009320 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -84,6 +84,11 @@ const goIdentifierCasingOverrides = new Map([ ]); const goCommentTextWrapLength = 90; const wrapGoCommentText = wordwrap(goCommentTextWrapLength); +const optionalNullableGoProperties = new Set([ + "ModelSwitchToRequest.autoTier", + "TaskClientUpdateProgress.percentage", + "TaskClientUpdateProgress.phase", +]); function goIdentifierWord(word: string, normalizeRest = false): string { const lower = word.toLowerCase(); @@ -396,6 +401,10 @@ function goJSONTag(jsonName: string, required: boolean, goType: string): string return `json:"${jsonName}${goJSONOmitSuffix(required, goType)}"`; } +function preserveOptionalNullableGoProperty(typeName: string, propName: string, goType: string): string { + return optionalNullableGoProperties.has(`${typeName}.${propName}`) ? `*${goType}` : goType; +} + async function formatGoFile(filePath: string): Promise { try { await execFileAsync("go", ["fmt", filePath]); @@ -1221,7 +1230,11 @@ function emitGoStruct( const prop = propSchema as JSONSchema7; const isReq = required.has(propName); const goName = toGoFieldName(propName); - const goType = resolveGoPropertyType(prop, typeName, propName, isReq, ctx); + const goType = preserveOptionalNullableGoProperty( + typeName, + propName, + resolveGoPropertyType(prop, typeName, propName, isReq, ctx) + ); if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); @@ -1947,7 +1960,11 @@ function emitGoFlatDiscriminatedUnion( continue; } const goName = toGoFieldName(propName); - const goType = resolveGoPropertyType(prop, variantTypeName, propName, required.has(propName), ctx); + const goType = preserveOptionalNullableGoProperty( + variantTypeName, + propName, + resolveGoPropertyType(prop, variantTypeName, propName, required.has(propName), ctx) + ); if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } From 3428b3655ea4cdb084650e6b566cb4793f1ea096 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 3 Sep 2026 23:25:45 -0400 Subject: [PATCH 52/60] fix(ci): address merge queue failure causes (#2515) Synchronize Go replay proxy startup, avoid nested PowerShell in the Windows shell E2E test, normalize bare abort results, retry truncated Python downloads, and prevent local Java validation from invoking Central publishing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/java-sdk-tests.yml | 1 + .../e2e/rpc_shell_and_fleet_e2e_test.go | 16 ++++++++----- go/internal/e2e/testharness/context.go | 9 ++++++++ python/copilot/_cli_download.py | 8 ++++--- python/test_cli_download.py | 23 ++++++++++++++++++- test/harness/replayingCapiProxy.test.ts | 5 ++-- test/harness/replayingCapiProxy.ts | 2 +- 7 files changed, 50 insertions(+), 14 deletions(-) diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index 0b6b62a0d9..514f17c758 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -410,6 +410,7 @@ jobs: mvn -B -pl copilot-native deploy -Prelease -DskipTests \ -Dcopilot.native.libc=glibc \ -Dcopilot.native.test.local.publication=true \ + -DskipPublishing=true \ "-Dcopilot.native.external.linux.arm64.classifier.path=$LINUX_ARM64_JAR" \ "-Dcopilot.native.external.win32.classifier.path=$WINDOWS_JAR" \ "-Dcopilot.native.external.win32.arm64.classifier.path=$WINDOWS_ARM64_JAR" \ diff --git a/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go b/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go index 81b8471dac..a426f6bdc9 100644 --- a/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go +++ b/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go @@ -30,12 +30,16 @@ func TestRPCShellAndFleetE2E(t *testing.T) { t.Fatalf("Failed to create session: %v", err) } - markerPath := filepath.Join(ctx.WorkDir, "shell-rpc-"+randomHex(t)+".txt") + commandDir := filepath.Join(ctx.WorkDir, "shell-rpc-"+randomHex(t)) + if err := os.Mkdir(commandDir, 0755); err != nil { + t.Fatalf("Failed to create shell command directory: %v", err) + } + markerPath := filepath.Join(commandDir, "marker.txt") const marker = "copilot-sdk-shell-rpc" - cwd := ctx.WorkDir + cwd := commandDir result, err := session.RPC.Shell.Exec(t.Context(), &rpc.ShellExecRequest{ - Command: writeFileCommand(markerPath, marker), + Command: writeFileCommand(filepath.Base(markerPath), marker), Cwd: &cwd, }) if err != nil { @@ -174,11 +178,11 @@ func randomHex(t *testing.T) string { return hex.EncodeToString(buf[:]) } -func writeFileCommand(markerPath, marker string) string { +func writeFileCommand(markerName, marker string) string { if runtime.GOOS == "windows" { - return fmt.Sprintf("powershell -NoLogo -NoProfile -Command \"Set-Content -LiteralPath '%s' -Value '%s'\"", markerPath, marker) + return fmt.Sprintf("echo %s>\"%s\"", marker, markerName) } - return fmt.Sprintf("sh -c \"printf '%%s' '%s' > '%s'\"", marker, markerPath) + return fmt.Sprintf("sh -c \"printf '%%s' '%s' > '%s'\"", marker, markerName) } func waitForFileText(t *testing.T, path, expected string) { diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index 0374cc5130..951adfc291 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -163,6 +163,15 @@ func NewTestContext(t *testing.T) *TestContext { os.RemoveAll(workDir) t.Fatalf("Failed to start proxy: %v", err) } + // Initialize the proxy before any client can start runtime requests. Tests that + // use a snapshot replace this empty configuration before model traffic begins. + dummySnapshotPath := filepath.Join(workDir, "__no_snapshot__.yaml") + if err := proxy.Configure(dummySnapshotPath, workDir); err != nil { + proxy.StopWithOptions(true) + os.RemoveAll(homeDir) + os.RemoveAll(workDir) + t.Fatalf("Failed to initialize proxy: %v", err) + } if err := proxy.SetCopilotUserByToken(defaultGitHubToken, map[string]interface{}{ "login": "e2e-test-user", "copilot_plan": "individual_pro", diff --git a/python/copilot/_cli_download.py b/python/copilot/_cli_download.py index 4477fcfff3..2d9076f6b8 100644 --- a/python/copilot/_cli_download.py +++ b/python/copilot/_cli_download.py @@ -27,6 +27,7 @@ import tempfile import time import zipfile +from http.client import IncompleteRead from pathlib import Path, PurePosixPath from urllib.error import HTTPError, URLError from urllib.request import urlopen @@ -43,6 +44,7 @@ _CACHE_DIR_NAME = "github-copilot-sdk" _MAX_RETRIES = 3 +_RETRIABLE_DOWNLOAD_ERRORS = (HTTPError, URLError, IncompleteRead) def _sanitize_version(version: str) -> str: @@ -128,7 +130,7 @@ def _fetch_checksums(version: str) -> dict[str, str]: with urlopen(url, timeout=30) as response: text = response.read().decode("utf-8") break - except (HTTPError, URLError) as exc: + except _RETRIABLE_DOWNLOAD_ERRORS as exc: last_exc = exc if attempt < _MAX_RETRIES - 1: time.sleep(2**attempt) @@ -255,7 +257,7 @@ def download_cli(version: str | None = None, *, force: bool = False) -> str: with urlopen(url, timeout=120) as response: data = response.read() break - except (HTTPError, URLError) as exc: + except _RETRIABLE_DOWNLOAD_ERRORS as exc: last_exc = exc if attempt < _MAX_RETRIES - 1: time.sleep(2**attempt) @@ -315,7 +317,7 @@ def _fetch_url_bytes(url: str, *, timeout: int) -> bytes: try: with urlopen(url, timeout=timeout) as response: return response.read() - except (HTTPError, URLError) as exc: + except _RETRIABLE_DOWNLOAD_ERRORS as exc: last_exc = exc if attempt < _MAX_RETRIES - 1: time.sleep(2**attempt) diff --git a/python/test_cli_download.py b/python/test_cli_download.py index aac652daf8..4ae4fe6bc3 100644 --- a/python/test_cli_download.py +++ b/python/test_cli_download.py @@ -7,7 +7,8 @@ import io import os import tarfile -from unittest.mock import patch +from http.client import IncompleteRead +from unittest.mock import MagicMock, patch import pytest @@ -41,6 +42,26 @@ def _runtime_package(npm_platform: str) -> bytes: return buffer.getvalue() +def test_fetch_url_bytes_retries_truncated_response(): + truncated_response = MagicMock() + truncated_response.__enter__.return_value.read.side_effect = IncompleteRead(b"partial", 4) + complete_response = MagicMock() + complete_response.__enter__.return_value.read.return_value = b"complete" + + with ( + patch.object( + _cli_download, + "urlopen", + side_effect=[truncated_response, complete_response], + ) as urlopen, + patch.object(_cli_download.time, "sleep") as sleep, + ): + assert _cli_download._fetch_url_bytes("https://example/runtime", timeout=30) == b"complete" + + assert urlopen.call_count == 2 + sleep.assert_called_once_with(1) + + class TestVerifyIntegrity: def test_accepts_matching_checksum(self): data = b"native-library-bytes" diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index c5ba6195b1..062fe89ae9 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -981,7 +981,7 @@ Always include PINEAPPLE_COCONUT_42. } }); - test("matches semantically equivalent interrupted shell results", async () => { + test("matches semantically equivalent interrupted tool results", async () => { const originalShellConfig = process.platform === "win32" ? ShellConfig.powerShell @@ -1056,8 +1056,7 @@ Always include PINEAPPLE_COCONUT_42. { role: "tool", tool_call_id: "runtime-call-id", - content: - "", + content: "Session aborted", }, ], }, diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index ca7f6d0f3f..9bcafcdad4 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -1556,7 +1556,7 @@ function normalizeAvailableToolNames(result: string): string { function normalizeInterruptedToolResult(result: string): string { return result.replace( - /^(?:Failed to execute `[^`]+` tool(?: with arguments: [\s\S]*?)? due to error: (?:Error: )?Session aborted||unknown attachedShellSession handle \d+)$/, + /^(?:Session aborted|Failed to execute `[^`]+` tool(?: with arguments: [\s\S]*?)? due to error: (?:Error: )?Session aborted||unknown attachedShellSession handle \d+)$/, "The execution of this tool, or a previous tool was interrupted.", ); } From 09210291cac77d58bbdf2a0c582fd57dc49cf1d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:36:33 +0000 Subject: [PATCH 53/60] Update @github/copilot to 1.0.83-5 (#2516) * Update Copilot CLI to 1.0.83-5 - Updated the Node.js CLI release pin - Re-ran code generators - Formatted generated code * Fix Java MCP server coverage test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Format Java MCP server coverage test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix transient Windows .NET timing failures Avoid surfacing a diagnostic stderr drain timeout after the owned process has exited, and allow Windows shell commands enough time to start before testing runtime timeout cleanup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Stephen Toub Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 8 +- dotnet/src/Generated/Rpc.cs | 250 ++++++++++++++++++ dotnet/src/Generated/SessionEvents.cs | 116 +++++++- dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs | 11 +- go/rpc/zrpc.go | 213 ++++++++++++--- go/rpc/zsession_events.go | 24 +- go/zsession_events.go | 7 + java/pom.xml | 2 +- java/scripts/codegen/package-lock.json | 92 ++++--- java/scripts/codegen/package.json | 2 +- .../copilot/generated/McpServerMetadata.java | 27 ++ .../generated/McpServersLoadedServer.java | 2 + .../copilot/generated/RemediationAction.java | 41 +++ .../copilot/generated/SessionErrorEvent.java | 2 + .../generated/SessionWarningEvent.java | 4 +- .../generated/ToolExecutionCompleteError.java | 4 +- .../rpc/AutopilotObjectiveCreditLimit.java | 31 +++ .../rpc/AutopilotObjectiveState.java | 41 +++ .../rpc/AutopilotObjectiveStatus.java | 37 +++ .../copilot/generated/rpc/McpServer.java | 4 +- .../generated/rpc/McpServerMetadata.java | 27 ++ .../rpc/PluginInstallStagingMode.java | 35 +++ .../generated/rpc/PluginsInstallResult.java | 2 + .../generated/rpc/RemediationAction.java | 41 +++ .../rpc/SessionAutopilotObjectiveApi.java | 42 +++ ...ssionAutopilotObjectiveGetStateParams.java | 30 +++ ...ssionAutopilotObjectiveGetStateResult.java | 30 +++ .../copilot/generated/rpc/SessionRpc.java | 3 + .../rpc/SlashCommandTimelineEntry.java | 4 +- .../rpc/GeneratedRpcRecordsCoverageTest.java | 6 +- nodejs/package.json | 2 +- nodejs/src/cliVersion.ts | 2 +- nodejs/src/generated/rpc.ts | 110 +++++++- nodejs/src/generated/session-events.ts | 43 ++- python/copilot/generated/rpc.py | 193 +++++++++++++- python/copilot/generated/session_events.py | 55 ++++ rust/src/generated/api_types.rs | 166 +++++++++++- rust/src/generated/rpc.rs | 43 +++ rust/src/generated/session_events.rs | 60 ++++- 39 files changed, 1689 insertions(+), 123 deletions(-) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/McpServerMetadata.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/RemediationAction.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerMetadata.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginInstallStagingMode.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/RemediationAction.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateResult.java diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 709121dd75..ea15df11cd 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -762,7 +762,13 @@ private static async Task CleanupCliProcessAsync(Process childProcess, ProcessSt s_stderrPumpShutdownTimeout); } - AddCleanupError(errors, ex, logger); + // Once the owned process has exited, stderr is diagnostic-only. A descendant + // can briefly retain the inherited pipe on Windows, but that must not turn a + // successful process shutdown into a client cleanup failure. + if (!processExited) + { + AddCleanupError(errors, ex, logger); + } } catch (Exception ex) { diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index d65aa4f1b8..2c74ab4bde 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -2921,6 +2921,10 @@ public sealed class PluginInstallResult /// Number of skills discovered and installed from the plugin. [JsonPropertyName("skillsInstalled")] public long SkillsInstalled { get; set; } + + /// Where the completed plugin tree was staged before atomic promotion. + [JsonPropertyName("stagingMode")] + public PluginInstallStagingMode? StagingMode { get; set; } } /// Plugin source and optional working directory for relative-path resolution. @@ -8561,6 +8565,80 @@ internal sealed class WorkspacesDiffRequest public string SessionId { get; set; } = string.Empty; } +/// Current per-window credit limit and consumption for an autopilot objective. +[Experimental(Diagnostics.Experimental)] +public sealed class AutopilotObjectiveCreditLimit +{ + /// Configured AI-credit cap, when one is set. + [JsonPropertyName("credits")] + public double? Credits { get; set; } + + /// Window consumption in fractional AI credits, for display. + [JsonPropertyName("creditsUsed")] + public double CreditsUsed { get; set; } + + /// Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + [RegularExpression("^[0-9]+$")] + [JsonPropertyName("creditsUsedNanoAiu")] + public string CreditsUsedNanoAiu { get; set; } = string.Empty; +} + +/// Public, persistence-independent projection of an autopilot objective. +[Experimental(Diagnostics.Experimental)] +public sealed class AutopilotObjectiveState +{ + /// Optional summary recorded when the objective completed. + [JsonPropertyName("completionSummary")] + public string? CompletionSummary { get; set; } + + /// Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. + [RegularExpression("^[0-9]+$")] + [JsonPropertyName("creditCountNanoAiu")] + public string CreditCountNanoAiu { get; set; } = string.Empty; + + /// Current per-window consumption and optional cap, when a credit-tracking window is present. + [JsonPropertyName("creditLimit")] + public AutopilotObjectiveCreditLimit? CreditLimit { get; set; } + + /// Session-local objective identifier. + [JsonPropertyName("id")] + public long Id { get; set; } + + /// User-provided objective text. + [JsonPropertyName("objective")] + public string Objective { get; set; } = string.Empty; + + /// Optional reason the objective is paused. + [JsonPropertyName("pauseReason")] + public string? PauseReason { get; set; } + + /// Current normalized lifecycle status. + [JsonPropertyName("status")] + public AutopilotObjectiveStatus Status { get; set; } + + /// Number of objective turns started. + [JsonPropertyName("turnCount")] + public long TurnCount { get; set; } +} + +/// Canonical runtime state for the session's current autopilot objective. +[Experimental(Diagnostics.Experimental)] +public sealed class AutopilotObjectiveGetStateResult +{ + /// Current objective state, or `null` when the session has no objective. + [JsonPropertyName("state")] + public AutopilotObjectiveState? State { get; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionAutopilotObjectiveGetStateRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Characters that, when typed in the composer, should trigger a `completions.request`. Empty when the session has no host-driven completions (e.g. local sessions, or a relay host that does not advertise `completionTriggerCharacters`). [Experimental(Diagnostics.Experimental)] public sealed class CompletionsGetTriggerCharactersResult @@ -9984,6 +10062,10 @@ public sealed class McpServer [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + [JsonPropertyName("serverMetadata")] + public McpServerMetadata? ServerMetadata { get; set; } + /// Configuration source: user, workspace, plugin, or builtin. [JsonPropertyName("source")] public McpServerSource? Source { get; set; } @@ -13894,6 +13976,10 @@ public partial class SlashCommandInvocationResultSelectSubcommand : SlashCommand [Experimental(Diagnostics.Experimental)] public sealed class SlashCommandTimelineEntry { + /// What the user must do to recover, when the entry reports a failure the runtime knows an action for. The `text` never names a client affordance, so a client that offers one renders it from this value. + [JsonPropertyName("remediation")] + public RemediationAction? Remediation { get; set; } + /// Text displayed for the timeline entry. [JsonPropertyName("text")] public string Text { get; set; } = string.Empty; @@ -22078,6 +22164,69 @@ public override void Write(Utf8JsonWriter writer, CatalogCandidateKind value, Js } +/// Where completed plugin content was staged before atomic promotion. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct PluginInstallStagingMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public PluginInstallStagingMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A sibling of the installed-plugins root, outside the recursively watched tree. + public static PluginInstallStagingMode External { get; } = new("external"); + + /// A sibling of the destination plugin directory, used when external staging is unavailable. + public static PluginInstallStagingMode DestinationSibling { get; } = new("destination_sibling"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PluginInstallStagingMode left, PluginInstallStagingMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PluginInstallStagingMode left, PluginInstallStagingMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is PluginInstallStagingMode other && Equals(other); + + /// + public bool Equals(PluginInstallStagingMode 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 PluginInstallStagingMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, PluginInstallStagingMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PluginInstallStagingMode)); + } + } +} + + /// Which tier this directory belongs to. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -25183,6 +25332,72 @@ public override void Write(Utf8JsonWriter writer, HistoryRewindUnavailableReason } +/// Current normalized autopilot objective lifecycle status. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AutopilotObjectiveStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AutopilotObjectiveStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The objective is actively running. + public static AutopilotObjectiveStatus Active { get; } = new("active"); + + /// The objective is paused and may be resumed. + public static AutopilotObjectiveStatus Paused { get; } = new("paused"); + + /// The objective completed. + public static AutopilotObjectiveStatus Completed { get; } = new("completed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AutopilotObjectiveStatus other && Equals(other); + + /// + public bool Equals(AutopilotObjectiveStatus 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 AutopilotObjectiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AutopilotObjectiveStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutopilotObjectiveStatus)); + } + } +} + + /// Whether task execution is synchronously awaited or managed in the background. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -32607,6 +32822,12 @@ internal SessionRpc(CopilotSession session) Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + /// AutopilotObjective APIs. + public AutopilotObjectiveApi AutopilotObjective => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// Completions APIs. public CompletionsApi Completions => field ?? @@ -33963,6 +34184,29 @@ public async Task DiffAsync(WorkspaceDiffMode mode, bool? i } } +/// Provides session-scoped AutopilotObjective APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class AutopilotObjectiveApi +{ + private readonly CopilotSession _session; + + internal AutopilotObjectiveApi(CopilotSession session) + { + _session = session; + } + + /// Reads the current canonical autopilot objective state for this session. + /// The to monitor for cancellation requests. The default is . + /// Canonical runtime state for the session's current autopilot objective. + public async Task GetStateAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionAutopilotObjectiveGetStateRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.autopilotObjective.getState", [request], cancellationToken); + } +} + /// Provides session-scoped Completions APIs. [Experimental(Diagnostics.Experimental)] public sealed class CompletionsApi @@ -37476,6 +37720,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.McpOauthWWWAuthenticateParams), TypeInfoPropertyName = "SessionEventsMcpOauthWWWAuthenticateParams")] [JsonSerializable(typeof(GitHub.Copilot.McpPromptsListChangedEvent), TypeInfoPropertyName = "SessionEventsMcpPromptsListChangedEvent")] [JsonSerializable(typeof(GitHub.Copilot.McpResourcesListChangedEvent), TypeInfoPropertyName = "SessionEventsMcpResourcesListChangedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.McpServerMetadata), TypeInfoPropertyName = "SessionEventsMcpServerMetadata")] [JsonSerializable(typeof(GitHub.Copilot.McpServerSource), TypeInfoPropertyName = "SessionEventsMcpServerSource")] [JsonSerializable(typeof(GitHub.Copilot.McpServerStatus), TypeInfoPropertyName = "SessionEventsMcpServerStatus")] [JsonSerializable(typeof(GitHub.Copilot.McpServerTransport), TypeInfoPropertyName = "SessionEventsMcpServerTransport")] @@ -37549,6 +37794,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.PromptCacheBreakData), TypeInfoPropertyName = "SessionEventsPromptCacheBreakData")] [JsonSerializable(typeof(GitHub.Copilot.PromptCacheBreakEvent), TypeInfoPropertyName = "SessionEventsPromptCacheBreakEvent")] [JsonSerializable(typeof(GitHub.Copilot.ReasoningSummary), TypeInfoPropertyName = "SessionEventsReasoningSummary")] +[JsonSerializable(typeof(GitHub.Copilot.RemediationAction), TypeInfoPropertyName = "SessionEventsRemediationAction")] [JsonSerializable(typeof(GitHub.Copilot.SamplingCompletedData), TypeInfoPropertyName = "SessionEventsSamplingCompletedData")] [JsonSerializable(typeof(GitHub.Copilot.SamplingCompletedEvent), TypeInfoPropertyName = "SessionEventsSamplingCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.SamplingRequestedData), TypeInfoPropertyName = "SessionEventsSamplingRequestedData")] @@ -37703,6 +37949,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(AuthIdentity))] [JsonSerializable(typeof(AuthInfo))] [JsonSerializable(typeof(AuthValidationError))] +[JsonSerializable(typeof(AutopilotObjectiveCreditLimit))] +[JsonSerializable(typeof(AutopilotObjectiveGetStateResult))] +[JsonSerializable(typeof(AutopilotObjectiveState))] [JsonSerializable(typeof(BuiltInModelCatalog))] [JsonSerializable(typeof(BuiltInModelCatalogEntry))] [JsonSerializable(typeof(BuiltinToolDescriptor))] @@ -38244,6 +38493,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionAuthLogoutUserRequest))] [JsonSerializable(typeof(SessionAuthStatus))] [JsonSerializable(typeof(SessionAuthSwitchRequest))] +[JsonSerializable(typeof(SessionAutopilotObjectiveGetStateRequest))] [JsonSerializable(typeof(SessionBulkDeleteResult))] [JsonSerializable(typeof(SessionCancelAllBackgroundAgentsRequest))] [JsonSerializable(typeof(SessionCanvasListOpenRequest))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index e7890dee92..58c8a2c9cc 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -2189,6 +2189,11 @@ public sealed partial class SessionErrorData [JsonPropertyName("providerCallId")] public string? ProviderCallId { get; set; } + /// What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("remediation")] + public RemediationAction? Remediation { get; set; } + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("serviceRequestId")] @@ -2352,6 +2357,11 @@ public sealed partial class SessionWarningData [JsonPropertyName("message")] public required string Message { get; set; } + /// What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("remediation")] + public RemediationAction? Remediation { get; set; } + /// Optional URL associated with this warning that the user can open in a browser. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("url")] @@ -7695,6 +7705,11 @@ public sealed partial class ToolExecutionCompleteError /// Human-readable error message. [JsonPropertyName("message")] public required string Message { get; set; } + + /// What the user must do to recover, when the runtime knows of an action. Set on sandbox policy denials, where `message` names the rule that blocked the call but never the client affordance that relaxes it. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("remediation")] + public RemediationAction? Remediation { get; set; } } /// Binary result returned by a tool for the model. @@ -8837,12 +8852,12 @@ public override bool? ManagedApprovalRequired [JsonPropertyName("possibleUrls")] public required PermissionRequestShellPossibleUrl[] PossibleUrls { get; set; } - /// True when the model has requested to run this command outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this command outside the sandbox, either because the command detaches and cannot be sandboxed at all, or because a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypass")] public bool? RequestSandboxBypass { get; set; } - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypassReason")] public string? RequestSandboxBypassReason { get; set; } @@ -8937,12 +8952,12 @@ public override bool? ManagedApprovalRequired [JsonPropertyName("path")] public required string Path { get; set; } - /// True when the model has requested to run this search outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to re-run this search outside the sandbox, after a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypass")] public bool? RequestSandboxBypass { get; set; } - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypassReason")] public string? RequestSandboxBypassReason { get; set; } @@ -9020,12 +9035,12 @@ public override bool? ManagedApprovalRequired [JsonPropertyName("redirectedFrom")] public string? RedirectedFrom { get; set; } - /// True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypass")] public bool? RequestSandboxBypass { get; set; } - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypassReason")] public string? RequestSandboxBypassReason { get; set; } @@ -9574,12 +9589,12 @@ public sealed partial class PermissionPromptRequestUrl : PermissionPromptRequest [JsonPropertyName("redirectedFrom")] public string? RedirectedFrom { get; set; } - /// True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypass")] public bool? RequestSandboxBypass { get; set; } - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypassReason")] public string? RequestSandboxBypassReason { get; set; } @@ -10503,6 +10518,15 @@ public sealed partial class CustomAgentsUpdatedAgent public required bool UserInvocable { get; set; } } +/// Server-advertised metadata learned through modern discovery or legacy initialization. +/// Nested data type for McpServerMetadata. +public sealed partial class McpServerMetadata +{ + /// Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. + [JsonPropertyName("instructions")] + public string? Instructions { get; set; } +} + /// A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata. /// Nested data type for McpServersLoadedServer. public sealed partial class McpServersLoadedServer @@ -10526,6 +10550,11 @@ public sealed partial class McpServersLoadedServer [JsonPropertyName("pluginVersion")] public string? PluginVersion { get; set; } + /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("serverMetadata")] + public McpServerMetadata? ServerMetadata { get; set; } + /// Configuration source: user, workspace, plugin, or builtin. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] @@ -10974,6 +11003,76 @@ public override void Write(Utf8JsonWriter writer, Verbosity value, JsonSerialize } } +/// What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct RemediationAction : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public RemediationAction(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Authenticate again with the Copilot backend. The current credential is absent, expired, or rejected. + public static RemediationAction SignIn { get; } = new("sign_in"); + + /// Authenticate as a different account. The current account exists but lacks access to the requested resource. + public static RemediationAction SwitchAccount { get; } = new("switch_account"); + + /// Inspect which account is currently authenticated before deciding what to change. + public static RemediationAction ShowAccount { get; } = new("show_account"); + + /// Review or widen the sandbox policy. The blocked path or host is named by the accompanying message or by the tool result the action arrived with. + public static RemediationAction ReviewSandboxPolicy { get; } = new("review_sandbox_policy"); + + /// Permit outbound network access in the sandbox policy. + public static RemediationAction AllowSandboxOutbound { get; } = new("allow_sandbox_outbound"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(RemediationAction left, RemediationAction right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(RemediationAction left, RemediationAction right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is RemediationAction other && Equals(other); + + /// + public bool Equals(RemediationAction 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 RemediationAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, RemediationAction value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemediationAction)); + } + } +} + /// The session mode the agent is operating in. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -16388,6 +16487,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(McpPromptsListChangedEvent))] [JsonSerializable(typeof(McpResourcesListChangedData))] [JsonSerializable(typeof(McpResourcesListChangedEvent))] +[JsonSerializable(typeof(McpServerMetadata))] [JsonSerializable(typeof(McpServersLoadedServer))] [JsonSerializable(typeof(McpToolsListChangedData))] [JsonSerializable(typeof(McpToolsListChangedEvent))] diff --git a/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs b/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs index cc78099527..25055a671a 100644 --- a/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs +++ b/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs @@ -28,16 +28,21 @@ public async Task Shell_Exec_With_Timeout_Kills_Long_Running_Command() var markerPath = Path.Join(Ctx.WorkDir, $"shell-timeout-{Guid.NewGuid():N}.txt"); var startedPath = Path.Join(Ctx.WorkDir, $"shell-timeout-started-{Guid.NewGuid():N}.txt"); - // Sleep 30s but timeout at 200ms — runtime should SIGTERM the child before the + // Sleep 30s but use a much shorter timeout — runtime should SIGTERM the child before the // sleep completes, which means the marker file must NEVER appear within a wait // window comfortably greater than the timeout but well under the sleep duration. + // Process startup on Windows can exceed 200ms on loaded runners, so match the + // platform-specific allowance used by the Rust coverage for this RPC. + var timeout = OperatingSystem.IsWindows() + ? TimeSpan.FromSeconds(2) + : TimeSpan.FromMilliseconds(200); var command = OperatingSystem.IsWindows() - ? $"echo started>\"{startedPath}\" & for /L %i in (1,1,2147483647) do @rem & echo should-not-exist>\"{markerPath}\"" + ? $"powershell -NoLogo -NoProfile -Command \"Set-Content -LiteralPath '{startedPath}' -Value started; Start-Sleep -Seconds 30; Set-Content -LiteralPath '{markerPath}' -Value should-not-exist\"" : $"printf 'started' > '{startedPath}'; sleep 30; printf 'should-not-exist' > '{markerPath}'"; // On Windows, terminating the shell wrapper can briefly leave children alive. // Keep this long-running command outside the fixture workspace so cleanup is not blocked by cwd handles. - var result = await session.Rpc.Shell.ExecAsync(command, cwd: Path.GetTempPath(), timeout: TimeSpan.FromMilliseconds(200)); + var result = await session.Rpc.Shell.ExecAsync(command, cwd: Path.GetTempPath(), timeout: timeout); Assert.False(string.IsNullOrWhiteSpace(result.ProcessId)); await TestHelper.WaitForConditionAsync( diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 4eb60ffd3c..3c9d2d46de 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -1070,6 +1070,49 @@ type AuthValidationError struct { // removed. type AuthValidationErrors []AuthValidationError +// Current per-window credit limit and consumption for an autopilot objective. +// Experimental: AutopilotObjectiveCreditLimit is part of an experimental API and may change +// or be removed. +type AutopilotObjectiveCreditLimit struct { + // Configured AI-credit cap, when one is set. + Credits *float64 `json:"credits,omitempty"` + // Window consumption in fractional AI credits, for display. + CreditsUsed float64 `json:"creditsUsed"` + // Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + CreditsUsedNanoAiu string `json:"creditsUsedNanoAiu"` +} + +// Canonical runtime state for the session's current autopilot objective. +// Experimental: AutopilotObjectiveGetStateResult is part of an experimental API and may +// change or be removed. +type AutopilotObjectiveGetStateResult struct { + // Current objective state, or `null` when the session has no objective. + State *AutopilotObjectiveState `json:"state"` +} + +// Public, persistence-independent projection of an autopilot objective. +// Experimental: AutopilotObjectiveState is part of an experimental API and may change or be +// removed. +type AutopilotObjectiveState struct { + // Optional summary recorded when the objective completed. + CompletionSummary *string `json:"completionSummary,omitempty"` + // Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a + // decimal string. + CreditCountNanoAiu string `json:"creditCountNanoAiu"` + // Current per-window consumption and optional cap, when a credit-tracking window is present. + CreditLimit *AutopilotObjectiveCreditLimit `json:"creditLimit,omitempty"` + // Session-local objective identifier. + ID int64 `json:"id"` + // User-provided objective text. + Objective string `json:"objective"` + // Optional reason the objective is paused. + PauseReason *string `json:"pauseReason,omitempty"` + // Current normalized lifecycle status. + Status AutopilotObjectiveStatus `json:"status"` + // Number of objective turns started. + TurnCount int64 `json:"turnCount"` +} + // The running runtime's complete catalog of well-known built-in model IDs, including // supported models and additional IDs with built-in metadata. // Experimental: BuiltInModelCatalog is part of an experimental API and may change or be @@ -6522,6 +6565,10 @@ type MCPServer struct { Error *string `json:"error,omitempty"` // Server name (config key) Name string `json:"name"` + // Server-advertised metadata for a connected server. Omitted when no live connection + // metadata is available, including while pending or when failed, disabled, stopped, or not + // configured. + ServerMetadata *MCPServerMetadata `json:"serverMetadata,omitempty"` // Configuration source: user, workspace, plugin, or builtin Source *MCPServerSource `json:"source,omitempty"` // Plugin name that provided this server, when source is plugin. @@ -6694,6 +6741,15 @@ type MCPServerList struct { Servers []MCPServer `json:"servers"` } +// Server-advertised metadata learned through modern discovery or legacy initialization. +// Experimental: MCPServerMetadata is part of an experimental API and may change or be +// removed. +type MCPServerMetadata struct { + // Non-empty natural-language guidance for using the server, or null when the server omitted + // instructions or advertised an empty string. + Instructions *string `json:"instructions"` +} + // Recorded MCP server pending-auth state. // Experimental: MCPServerNeedsAuthInfo is part of an experimental API and may change or be // removed. @@ -9011,6 +9067,8 @@ type PluginInstallResult struct { PostInstallMessage *string `json:"postInstallMessage,omitempty"` // Number of skills discovered and installed from the plugin SkillsInstalled int64 `json:"skillsInstalled"` + // Where the completed plugin tree was staged before atomic promotion + StagingMode *PluginInstallStagingMode `json:"stagingMode,omitempty"` } // Plugins installed for the session, with their enabled state and version metadata. @@ -14155,6 +14213,10 @@ type SlashCommandSelectSubcommandOption struct { // Experimental: SlashCommandTimelineEntry is part of an experimental API and may change or // be removed. type SlashCommandTimelineEntry struct { + // What the user must do to recover, when the entry reports a failure the runtime knows an + // action for. The `text` never names a client affordance, so a client that offers one + // renders it from this value. + Remediation *RemediationAction `json:"remediation,omitempty"` // Text displayed for the timeline entry. Text string `json:"text"` // Timeline entry presentation type. @@ -16429,6 +16491,20 @@ const ( AuthInfoTypeUser AuthInfoType = "user" ) +// Current normalized autopilot objective lifecycle status. +// Experimental: AutopilotObjectiveStatus is part of an experimental API and may change or +// be removed. +type AutopilotObjectiveStatus string + +const ( + // The objective is actively running. + AutopilotObjectiveStatusActive AutopilotObjectiveStatus = "active" + // The objective completed. + AutopilotObjectiveStatusCompleted AutopilotObjectiveStatus = "completed" + // The objective is paused and may be resumed. + AutopilotObjectiveStatusPaused AutopilotObjectiveStatus = "paused" +) + // Routing preference used when the session model is `auto`. // Experimental: AutoTier is part of an experimental API and may change or be removed. type AutoTier string @@ -18495,6 +18571,18 @@ const ( PermissionsSetApproveAllSourceUserSetting PermissionsSetApproveAllSource = "user_setting" ) +// Where completed plugin content was staged before atomic promotion. +// Experimental: PluginInstallStagingMode is part of an experimental API and may change or +// be removed. +type PluginInstallStagingMode string + +const ( + // A sibling of the destination plugin directory, used when external staging is unavailable. + PluginInstallStagingModeDestinationSibling PluginInstallStagingMode = "destination_sibling" + // A sibling of the installed-plugins root, outside the recursively watched tree. + PluginInstallStagingModeExternal PluginInstallStagingMode = "external" +) + // Controls whether the runtime may defer loading an external tool definition. // Experimental: ProtocolExternalToolDefer is part of an experimental API and may change or // be removed. @@ -18644,6 +18732,31 @@ const ( ReasoningSummaryNone ReasoningSummary = "none" ) +// What the user must do to recover from a failure, named as an action rather than as one +// client's affordance. The runtime cannot know which affordance a client offers — a slash +// command, a settings pane, a link — so the accompanying message stays host-agnostic and +// each client renders its own copy from this value. Absent when the runtime knows of no +// action the user can take. +// Experimental: RemediationAction is part of an experimental API and may change or be +// removed. +type RemediationAction string + +const ( + // Permit outbound network access in the sandbox policy. + RemediationActionAllowSandboxOutbound RemediationAction = "allow_sandbox_outbound" + // Review or widen the sandbox policy. The blocked path or host is named by the accompanying + // message or by the tool result the action arrived with. + RemediationActionReviewSandboxPolicy RemediationAction = "review_sandbox_policy" + // Inspect which account is currently authenticated before deciding what to change. + RemediationActionShowAccount RemediationAction = "show_account" + // Authenticate again with the Copilot backend. The current credential is absent, expired, + // or rejected. + RemediationActionSignIn RemediationAction = "sign_in" + // Authenticate as a different account. The current account exists but lacks access to the + // requested resource. + RemediationActionSwitchAccount RemediationAction = "switch_account" +) + // State discriminator for RemoteControlStatus. type RemoteControlStatusState string @@ -21926,6 +22039,28 @@ func (a *AgentAPI) SetPrompt(ctx context.Context, params *AgentSetPromptRequest) return &result, nil } +// Experimental: AutopilotObjectiveAPI contains experimental APIs that may change or be +// removed. +type AutopilotObjectiveAPI sessionAPI + +// GetState reads the current canonical autopilot objective state for this session. +// +// RPC method: session.autopilotObjective.getState. +// +// Returns: Canonical runtime state for the session's current autopilot objective. +func (a *AutopilotObjectiveAPI) GetState(ctx context.Context) (*AutopilotObjectiveGetStateResult, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.autopilotObjective.getState", req) + if err != nil { + return nil, err + } + var result AutopilotObjectiveGetStateResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: CanvasAPI contains experimental APIs that may change or be removed. type CanvasAPI sessionAPI @@ -27496,44 +27631,45 @@ type SessionRPC struct { // Reuse a single struct instead of allocating one for each service on the heap. common sessionAPI - Agent *AgentAPI - Canvas *CanvasAPI - Commands *CommandsAPI - Completions *CompletionsAPI - ContentExclusion *ContentExclusionAPI - Debug *DebugAPI - EventLog *EventLogAPI - Extensions *ExtensionsAPI - Factory *FactoryAPI - Fleet *FleetAPI - GitHubAuth *GitHubAuthAPI - History *HistoryAPI - Instructions *InstructionsAPI - LimitPrediction *LimitPredictionAPI - Lsp *LspAPI - MCP *MCPAPI - Metadata *MetadataAPI - Mode *ModeAPI - Model *ModelAPI - Name *NameAPI - Options *OptionsAPI - Permissions *PermissionsAPI - Plan *PlanAPI - Plugins *PluginsAPI - Provider *ProviderAPI - Queue *QueueAPI - Remote *RemoteAPI - Sandbox *SandboxAPI - Schedule *ScheduleAPI - Shell *ShellAPI - Skills *SkillsAPI - Tasks *TasksAPI - Telemetry *TelemetryAPI - Tools *ToolsAPI - UI *UIAPI - Usage *UsageAPI - Visibility *VisibilityAPI - Workspaces *WorkspacesAPI + Agent *AgentAPI + AutopilotObjective *AutopilotObjectiveAPI + Canvas *CanvasAPI + Commands *CommandsAPI + Completions *CompletionsAPI + ContentExclusion *ContentExclusionAPI + Debug *DebugAPI + EventLog *EventLogAPI + Extensions *ExtensionsAPI + Factory *FactoryAPI + Fleet *FleetAPI + GitHubAuth *GitHubAuthAPI + History *HistoryAPI + Instructions *InstructionsAPI + LimitPrediction *LimitPredictionAPI + Lsp *LspAPI + MCP *MCPAPI + Metadata *MetadataAPI + Mode *ModeAPI + Model *ModelAPI + Name *NameAPI + Options *OptionsAPI + Permissions *PermissionsAPI + Plan *PlanAPI + Plugins *PluginsAPI + Provider *ProviderAPI + Queue *QueueAPI + Remote *RemoteAPI + Sandbox *SandboxAPI + Schedule *ScheduleAPI + Shell *ShellAPI + Skills *SkillsAPI + Tasks *TasksAPI + Telemetry *TelemetryAPI + Tools *ToolsAPI + UI *UIAPI + Usage *UsageAPI + Visibility *VisibilityAPI + Workspaces *WorkspacesAPI } // Aborts the current agent turn. @@ -27819,6 +27955,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r := &SessionRPC{} r.common = sessionAPI{client: client, sessionID: sessionID} r.Agent = (*AgentAPI)(&r.common) + r.AutopilotObjective = (*AutopilotObjectiveAPI)(&r.common) r.Canvas = (*CanvasAPI)(&r.common) r.Commands = (*CommandsAPI)(&r.common) r.Completions = (*CompletionsAPI)(&r.common) diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index ccddffb360..2874e1f59b 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -920,6 +920,8 @@ type SessionErrorData struct { Message string `json:"message"` // GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs ProviderCallID *string `json:"providerCallId,omitempty"` + // What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + Remediation *RemediationAction `json:"remediation,omitempty"` // Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation ServiceRequestID *string `json:"serviceRequestId,omitempty"` // Error stack trace, when available @@ -2868,6 +2870,8 @@ func (*ToolUserRequestedData) Type() SessionEventType { return SessionEventTypeT type SessionWarningData struct { // Human-readable warning message for display in the timeline Message string `json:"message"` + // What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + Remediation *RemediationAction `json:"remediation,omitempty"` // Optional URL associated with this warning that the user can open in a browser URL *string `json:"url,omitempty"` // Category of warning (e.g., "subscription", "policy", "mcp") @@ -3506,6 +3510,8 @@ type MCPServersLoadedServer struct { PluginName *string `json:"pluginName,omitempty"` // Version of the plugin that supplied the effective MCP server config, only when source is plugin PluginVersion *string `json:"pluginVersion,omitempty"` + // Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + ServerMetadata *MCPServerMetadata `json:"serverMetadata,omitempty"` // Configuration source: user, workspace, plugin, or builtin Source *MCPServerSource `json:"source,omitempty"` // Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured @@ -3827,9 +3833,9 @@ type PermissionPromptRequestURL struct { ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` // Immediately preceding URL when this prompt is for a redirect target RedirectedFrom *string `json:"redirectedFrom,omitempty"` - // True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + // True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` - // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + // What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` @@ -4083,9 +4089,9 @@ type PermissionRequestRead struct { ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` // Path of the file or directory being read Path string `json:"path"` - // True when the model has requested to run this search outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + // True when the tool is asking to re-run this search outside the sandbox, after a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` - // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + // What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` @@ -4116,9 +4122,9 @@ type PermissionRequestShell struct { PossiblePaths []string `json:"possiblePaths"` // URLs that may be accessed by the command PossibleURLs []PermissionRequestShellPossibleURL `json:"possibleUrls"` - // True when the model has requested to run this command outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + // True when the tool is asking to run this command outside the sandbox, either because the command detaches and cannot be sandboxed at all, or because a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` - // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + // What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` @@ -4139,9 +4145,9 @@ type PermissionRequestURL struct { ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` // Immediately preceding URL when this request is for a redirect target RedirectedFrom *string `json:"redirectedFrom,omitempty"` - // True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + // True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` - // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + // What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` @@ -4826,6 +4832,8 @@ type ToolExecutionCompleteError struct { Code *string `json:"code,omitempty"` // Human-readable error message Message string `json:"message"` + // What the user must do to recover, when the runtime knows of an action. Set on sandbox policy denials, where `message` names the rule that blocked the call but never the client affordance that relaxes it. + Remediation *RemediationAction `json:"remediation,omitempty"` } // Tool execution result on success diff --git a/go/zsession_events.go b/go/zsession_events.go index 8ac49affba..9540968e52 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -169,6 +169,7 @@ type ( MCPOauthWwwAuthenticateParams = rpc.MCPOauthWwwAuthenticateParams MCPPromptsListChangedData = rpc.MCPPromptsListChangedData MCPResourcesListChangedData = rpc.MCPResourcesListChangedData + MCPServerMetadata = rpc.MCPServerMetadata MCPServersLoadedServer = rpc.MCPServersLoadedServer MCPServerSource = rpc.MCPServerSource MCPServerStatus = rpc.MCPServerStatus @@ -258,6 +259,7 @@ type ( RawSystemNotification = rpc.RawSystemNotification RawToolExecutionCompleteContent = rpc.RawToolExecutionCompleteContent ReasoningSummary = rpc.ReasoningSummary + RemediationAction = rpc.RemediationAction SamplingCompletedData = rpc.SamplingCompletedData SamplingRequestedData = rpc.SamplingRequestedData SandboxDecisionData = rpc.SandboxDecisionData @@ -697,6 +699,11 @@ const ( ReasoningSummaryConcise = rpc.ReasoningSummaryConcise ReasoningSummaryDetailed = rpc.ReasoningSummaryDetailed ReasoningSummaryNone = rpc.ReasoningSummaryNone + RemediationActionAllowSandboxOutbound = rpc.RemediationActionAllowSandboxOutbound + RemediationActionReviewSandboxPolicy = rpc.RemediationActionReviewSandboxPolicy + RemediationActionShowAccount = rpc.RemediationActionShowAccount + RemediationActionSignIn = rpc.RemediationActionSignIn + RemediationActionSwitchAccount = rpc.RemediationActionSwitchAccount ScheduleOriginModel = rpc.ScheduleOriginModel ScheduleOriginUser = rpc.ScheduleOriginUser SessionEventTypeAbort = rpc.SessionEventTypeAbort diff --git a/java/pom.xml b/java/pom.xml index bf21d05af4..e12b967d42 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -63,7 +63,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.83-4 + ^1.0.83-5 true diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index a394c2717f..5e10cd839b 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.83-4", + "@github/copilot": "^1.0.83-5", "json-schema": "^0.4.0", "tsx": "^4.23.13" } @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.83-4", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-4.tgz", - "integrity": "sha512-IiUDou0khxU8hu3Xjfq0uwp08qqZwJROizROC+ZMUZaUZGOMD64UXi4N/hauosTEnL/kI/WxzYKw4kO7W6LtpQ==", + "version": "1.0.83-5", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-5.tgz", + "integrity": "sha512-WtyV+Uom2EHR9agUeshRH9aUgvp4zEvAgY0k+DcD9qJKJ8nzPfGXpMrcOJF37CsvxQ3p8Djcvg4aY2ys3XM9CA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-4", - "@github/copilot-darwin-x64": "1.0.83-4", - "@github/copilot-linux-arm64": "1.0.83-4", - "@github/copilot-linux-x64": "1.0.83-4", - "@github/copilot-linuxmusl-arm64": "1.0.83-4", - "@github/copilot-linuxmusl-x64": "1.0.83-4", - "@github/copilot-win32-arm64": "1.0.83-4", - "@github/copilot-win32-x64": "1.0.83-4" + "@github/copilot-darwin-arm64": "1.0.83-5", + "@github/copilot-darwin-x64": "1.0.83-5", + "@github/copilot-linux-arm64": "1.0.83-5", + "@github/copilot-linux-x64": "1.0.83-5", + "@github/copilot-linuxmusl-arm64": "1.0.83-5", + "@github/copilot-linuxmusl-x64": "1.0.83-5", + "@github/copilot-win32-arm64": "1.0.83-5", + "@github/copilot-win32-x64": "1.0.83-5" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-4", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-4.tgz", - "integrity": "sha512-rImrvW6dGC16Fu2MLPGNyxb1Nav9n+E5o4hKqyzC1NvK+9kknH8vPTosDEJt4+QEYPCGqyicTjfEoFvQjam6gg==", + "version": "1.0.83-5", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-5.tgz", + "integrity": "sha512-pj4yCrsVs7Nj16ogROYSKVyMDsmNMt4DRaiQEKUVhR9/Vs5n5DG+N55M2B1chT5wSk5MKtB9bov5Ep+x3jMU7g==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-4", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-4.tgz", - "integrity": "sha512-w7ZGwEwDutVLBfkO0WcJaHuNpv/OZxHbtj0cAzGFHYt6NiC7Pk/VQ9X+OAZBlVby/75yCC0Lf2SlIHM7/4Weqw==", + "version": "1.0.83-5", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-5.tgz", + "integrity": "sha512-mU2TBR7zW9kIfM6+r2W+empF0O3J+M/EOKja04IWx4EMc/XQVRnRRdeDaQAPfVF5prEBLxxqe90Ctq6a14IHEA==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-4", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-4.tgz", - "integrity": "sha512-UksdtQRk+sYVNP+X0VhGDhmmOE1kW9Bmnkj+1zLspwRxuzAZwLY5LXzrZ0/HIw5s6xQKDTWJKGWPlRvipK3mrQ==", + "version": "1.0.83-5", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-5.tgz", + "integrity": "sha512-UG5X20iRyV6A/7tKiVjh/CcqoXDEKvvKd3zcH0l8LEkXhHNGmSY6WiF3FOvp3R5tFhwr2L97UIq8JoKJwUG9TA==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-4", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-4.tgz", - "integrity": "sha512-Pxc6f9ear0vt0oMkG1tMxxDKQBsazaUiU7fcWYlfA1qBog0Q+UYoFxAmENNVPREPZ71nwM7P6gW7iO/GdMfYug==", + "version": "1.0.83-5", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-5.tgz", + "integrity": "sha512-sv0CwXtP1gyjDo2JbXC0OCIdMhKgK1ngaVc6HNIdIJpNZebMNTbe8QRxuV7DjXu9tSGP+tL9Kq3nPp/RipZq1Q==", "cpu": [ "x64" ], @@ -513,10 +513,26 @@ "copilot-linux-x64": "copilot" } }, + "node_modules/@github/copilot-linuxmusl-arm64": { + "version": "1.0.83-5", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-5.tgz", + "integrity": "sha512-orid/rhuGwypXypYCYDvbffY9CI0JPHntMUzuTP8lUQg45UsSUv3AtJOQPH3Rh3IGr7WJxG5lSxv/oKVv/+H7A==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linuxmusl-arm64": "copilot" + } + }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-4", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-4.tgz", - "integrity": "sha512-Gg6amLZ6M2kxVSCghLcFpT+T/mwIJLK/oV5XyHZW0wnsYaoaeTlAUpJJ+K4JwRxyxTeFGxRV7woUmY3iWRU1EQ==", + "version": "1.0.83-5", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-5.tgz", + "integrity": "sha512-bcc5seLXLitC3+bH6QTzgFYBZPOmhiKHfkehPGwSdJ5S0l+D5GfuwVfNp7x+W0DDSF/2wXWKF2j6iTCruA45Zg==", "cpu": [ "x64" ], @@ -530,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-4", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-4.tgz", - "integrity": "sha512-ZYqGCzJyQlw4DquuIJQAKEud9OpNizOgpTwoLTgf6ej3glqM+ISu8iVIQ9EQFhaI6m9KrXJ4iwCpFjUwv7zWDQ==", + "version": "1.0.83-5", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-5.tgz", + "integrity": "sha512-nBFaP31NfWh787eLra3QqgSoEmd7wJGSKg7ANvsY8XDOC5lns8yQBm5y4WVvWNBCVwb196NEwInwGUHDXbj2cw==", "cpu": [ "arm64" ], @@ -545,6 +561,22 @@ "copilot-win32-arm64": "copilot.exe" } }, + "node_modules/@github/copilot-win32-x64": { + "version": "1.0.83-5", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-5.tgz", + "integrity": "sha512-DlxPHBRal+5oHyQ7f/ChB8zrc71eIpqxd7CzXFU5kcU0YzskitnIp9CLsymqWTEdSoWSNq9+7bhKemIUHWTkfA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "copilot-win32-x64": "copilot.exe" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 4010551edd..5ef4484fb7 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.83-4", + "@github/copilot": "^1.0.83-5", "json-schema": "^0.4.0", "tsx": "^4.23.13" } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerMetadata.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerMetadata.java new file mode 100644 index 0000000000..6657eb5d87 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerMetadata.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Server-advertised metadata learned through modern discovery or legacy initialization. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpServerMetadata( + /** Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. */ + @JsonProperty("instructions") String instructions +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java index c4567f30fa..ca16f83ece 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java @@ -29,6 +29,8 @@ public record McpServersLoadedServer( @JsonProperty("source") McpServerSource source, /** Error message if the server failed to connect */ @JsonProperty("error") String error, + /** Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. */ + @JsonProperty("serverMetadata") McpServerMetadata serverMetadata, /** Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server) */ @JsonProperty("transport") McpServerTransport transport, /** Name of the plugin that supplied the effective MCP server config, only when source is plugin */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/RemediationAction.java b/java/sdk/src/generated/java/com/github/copilot/generated/RemediationAction.java new file mode 100644 index 0000000000..5fba9dfc33 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/RemediationAction.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 javax.annotation.processing.Generated; + +/** + * What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum RemediationAction { + /** The {@code sign_in} variant. */ + SIGN_IN("sign_in"), + /** The {@code switch_account} variant. */ + SWITCH_ACCOUNT("switch_account"), + /** The {@code show_account} variant. */ + SHOW_ACCOUNT("show_account"), + /** The {@code review_sandbox_policy} variant. */ + REVIEW_SANDBOX_POLICY("review_sandbox_policy"), + /** The {@code allow_sandbox_outbound} variant. */ + ALLOW_SANDBOX_OUTBOUND("allow_sandbox_outbound"); + + private final String value; + RemediationAction(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static RemediationAction fromValue(String value) { + for (RemediationAction v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown RemediationAction value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionErrorEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionErrorEvent.java index cd7f34365f..3ef32dfd13 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionErrorEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionErrorEvent.java @@ -42,6 +42,8 @@ public record SessionErrorEventData( @JsonProperty("eligibleForAutoSwitch") Boolean eligibleForAutoSwitch, /** Human-readable error message */ @JsonProperty("message") String message, + /** What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. */ + @JsonProperty("remediation") RemediationAction remediation, /** Error stack trace, when available */ @JsonProperty("stack") String stack, /** HTTP status code from the upstream request, if applicable */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionWarningEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionWarningEvent.java index a253f246ed..5a76d8fa9e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionWarningEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionWarningEvent.java @@ -39,7 +39,9 @@ public record SessionWarningEventData( /** Human-readable warning message for display in the timeline */ @JsonProperty("message") String message, /** Optional URL associated with this warning that the user can open in a browser */ - @JsonProperty("url") String url + @JsonProperty("url") String url, + /** What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. */ + @JsonProperty("remediation") RemediationAction remediation ) { } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteError.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteError.java index ac4c7d843d..8e183b78d1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteError.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteError.java @@ -24,6 +24,8 @@ public record ToolExecutionCompleteError( /** Human-readable error message */ @JsonProperty("message") String message, /** Machine-readable error code */ - @JsonProperty("code") String code + @JsonProperty("code") String code, + /** What the user must do to recover, when the runtime knows of an action. Set on sandbox policy denials, where `message` names the rule that blocked the call but never the client affordance that relaxes it. */ + @JsonProperty("remediation") RemediationAction remediation ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java new file mode 100644 index 0000000000..d3f6a585b8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Current per-window credit limit and consumption for an autopilot objective. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AutopilotObjectiveCreditLimit( + /** Configured AI-credit cap, when one is set. */ + @JsonProperty("credits") Double credits, + /** Window consumption in fractional AI credits, for display. */ + @JsonProperty("creditsUsed") Double creditsUsed, + /** Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. */ + @JsonProperty("creditsUsedNanoAiu") String creditsUsedNanoAiu +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java new file mode 100644 index 0000000000..657884c4f6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Public, persistence-independent projection of an autopilot objective. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AutopilotObjectiveState( + /** Session-local objective identifier. */ + @JsonProperty("id") Long id, + /** User-provided objective text. */ + @JsonProperty("objective") String objective, + /** Current normalized lifecycle status. */ + @JsonProperty("status") AutopilotObjectiveStatus status, + /** Number of objective turns started. */ + @JsonProperty("turnCount") Long turnCount, + /** Optional reason the objective is paused. */ + @JsonProperty("pauseReason") String pauseReason, + /** Optional summary recorded when the objective completed. */ + @JsonProperty("completionSummary") String completionSummary, + /** Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. */ + @JsonProperty("creditCountNanoAiu") String creditCountNanoAiu, + /** Current per-window consumption and optional cap, when a credit-tracking window is present. */ + @JsonProperty("creditLimit") AutopilotObjectiveCreditLimit creditLimit +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java new file mode 100644 index 0000000000..b50cf2072c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Current normalized autopilot objective lifecycle status. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutopilotObjectiveStatus { + /** The {@code active} variant. */ + ACTIVE("active"), + /** The {@code paused} variant. */ + PAUSED("paused"), + /** The {@code completed} variant. */ + COMPLETED("completed"); + + private final String value; + AutopilotObjectiveStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AutopilotObjectiveStatus fromValue(String value) { + for (AutopilotObjectiveStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutopilotObjectiveStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java index 14a9118d0b..063385f6ff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java @@ -32,6 +32,8 @@ public record McpServer( /** Plugin version that provided this server, when source is plugin. */ @JsonProperty("sourcePluginVersion") String sourcePluginVersion, /** Error message if the server failed to connect */ - @JsonProperty("error") String error + @JsonProperty("error") String error, + /** Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. */ + @JsonProperty("serverMetadata") McpServerMetadata serverMetadata ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerMetadata.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerMetadata.java new file mode 100644 index 0000000000..15ba7395d6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerMetadata.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +/** + * Server-advertised metadata learned through modern discovery or legacy initialization. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpServerMetadata( + /** Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. */ + @JsonProperty("instructions") String instructions +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginInstallStagingMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginInstallStagingMode.java new file mode 100644 index 0000000000..e754f07bc9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginInstallStagingMode.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * Where completed plugin content was staged before atomic promotion. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum PluginInstallStagingMode { + /** The {@code external} variant. */ + EXTERNAL("external"), + /** The {@code destination_sibling} variant. */ + DESTINATION_SIBLING("destination_sibling"); + + private final String value; + PluginInstallStagingMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static PluginInstallStagingMode fromValue(String value) { + for (PluginInstallStagingMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown PluginInstallStagingMode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginsInstallResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginsInstallResult.java index 82152cfe4c..a1e04692c6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginsInstallResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginsInstallResult.java @@ -28,6 +28,8 @@ public record PluginsInstallResult( @JsonProperty("plugin") InstalledPluginInfo plugin, /** Number of skills discovered and installed from the plugin */ @JsonProperty("skillsInstalled") Long skillsInstalled, + /** Where the completed plugin tree was staged before atomic promotion */ + @JsonProperty("stagingMode") PluginInstallStagingMode stagingMode, /** Optional post-install message provided by the plugin (e.g. setup instructions) */ @JsonProperty("postInstallMessage") String postInstallMessage, /** Set when the install path is deprecated (e.g. direct repo / URL / local installs). Callers should surface this to end users. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RemediationAction.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RemediationAction.java new file mode 100644 index 0000000000..50f6256352 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RemediationAction.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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 javax.annotation.processing.Generated; + +/** + * What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum RemediationAction { + /** The {@code sign_in} variant. */ + SIGN_IN("sign_in"), + /** The {@code switch_account} variant. */ + SWITCH_ACCOUNT("switch_account"), + /** The {@code show_account} variant. */ + SHOW_ACCOUNT("show_account"), + /** The {@code review_sandbox_policy} variant. */ + REVIEW_SANDBOX_POLICY("review_sandbox_policy"), + /** The {@code allow_sandbox_outbound} variant. */ + ALLOW_SANDBOX_OUTBOUND("allow_sandbox_outbound"); + + private final String value; + RemediationAction(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static RemediationAction fromValue(String value) { + for (RemediationAction v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown RemediationAction value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java new file mode 100644 index 0000000000..ab4058e6b6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * 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 autopilotObjective} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionAutopilotObjectiveApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionAutopilotObjectiveApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getState() { + return caller.invoke("session.autopilotObjective.getState", java.util.Map.of("sessionId", this.sessionId), SessionAutopilotObjectiveGetStateResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateParams.java new file mode 100644 index 0000000000..94ca56d3b5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateParams.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; + +/** + * Identifies the target session. + * + * @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 SessionAutopilotObjectiveGetStateParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateResult.java new file mode 100644 index 0000000000..d4d2185365 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateResult.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; + +/** + * Canonical runtime state for the session's current autopilot objective. + * + * @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 SessionAutopilotObjectiveGetStateResult( + /** Current objective state, or `null` when the session has no objective. */ + @JsonProperty("state") AutopilotObjectiveState state +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java index b9bd6b8226..cf89d37208 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java @@ -49,6 +49,8 @@ public final class SessionRpc { public final SessionPlanApi plan; /** API methods for the {@code workspaces} namespace. */ public final SessionWorkspacesApi workspaces; + /** API methods for the {@code autopilotObjective} namespace. */ + public final SessionAutopilotObjectiveApi autopilotObjective; /** API methods for the {@code completions} namespace. */ public final SessionCompletionsApi completions; /** API methods for the {@code instructions} namespace. */ @@ -127,6 +129,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.name = new SessionNameApi(caller, sessionId); this.plan = new SessionPlanApi(caller, sessionId); this.workspaces = new SessionWorkspacesApi(caller, sessionId); + this.autopilotObjective = new SessionAutopilotObjectiveApi(caller, sessionId); this.completions = new SessionCompletionsApi(caller, sessionId); this.instructions = new SessionInstructionsApi(caller, sessionId); this.fleet = new SessionFleetApi(caller, sessionId); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandTimelineEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandTimelineEntry.java index a238d7fb67..f8c832e05d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandTimelineEntry.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandTimelineEntry.java @@ -21,6 +21,8 @@ public record SlashCommandTimelineEntry( /** Text displayed for the timeline entry. */ @JsonProperty("text") String text, /** Optional URL associated with the timeline entry. */ - @JsonProperty("url") String url + @JsonProperty("url") String url, + /** What the user must do to recover, when the entry reports a failure the runtime knows an action for. The `text` never names a client affordance, so a client that offers one renders it from this value. */ + @JsonProperty("remediation") RemediationAction remediation ) { } diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 1702561bdc..938e0609f6 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -638,12 +638,16 @@ void sessionLogResult_record() { @Test void sessionMcpListResult_nested() { - var server = new McpServer("my-mcp", McpServerStatus.CONNECTED, McpServerSource.USER, null, null, null); + var metadata = new McpServerMetadata("Use this server for repository operations."); + var server = new McpServer("my-mcp", McpServerStatus.CONNECTED, McpServerSource.USER, null, null, null, + metadata); var result = new SessionMcpListResult(List.of(server), null); assertEquals(1, result.servers().size()); assertEquals("my-mcp", result.servers().get(0).name()); assertEquals(McpServerStatus.CONNECTED, result.servers().get(0).status()); assertEquals(McpServerSource.USER, result.servers().get(0).source()); + assertEquals("Use this server for repository operations.", + result.servers().get(0).serverMetadata().instructions()); } @Test diff --git a/nodejs/package.json b/nodejs/package.json index f64c05897e..7bb3159f73 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/github/copilot-sdk.git" }, "version": "0.0.0-dev", - "copilotCliVersion": "1.0.83-4", + "copilotCliVersion": "1.0.83-5", "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", "main": "./dist/cjs/index.js", "types": "./dist/index.d.ts", diff --git a/nodejs/src/cliVersion.ts b/nodejs/src/cliVersion.ts index c4ad7b3045..60a4ba864d 100644 --- a/nodejs/src/cliVersion.ts +++ b/nodejs/src/cliVersion.ts @@ -1,3 +1,3 @@ -export const COPILOT_CLI_VERSION = "1.0.83-4"; +export const COPILOT_CLI_VERSION = "1.0.83-5"; export const COPILOT_CLI_USE_NPM_PACKAGE = false; diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 322c13731b..f4978de1ff 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, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; +import type { AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; /** A value that can be represented losslessly on the SDK JSON wire. */ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; @@ -276,6 +276,20 @@ export type AuthInfoType = */ /** @experimental */ export type AuthValidationErrors = AuthValidationError[]; +/** + * Current normalized autopilot objective lifecycle status. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AutopilotObjectiveStatus". + */ +/** @experimental */ +export type AutopilotObjectiveStatus = + /** The objective is actively running. */ + | "active" + /** The objective is paused and may be resumed. */ + | "paused" + /** The objective completed. */ + | "completed"; /** * Root JSON Schema type for a built-in tool input. * @@ -2726,6 +2740,18 @@ export type PermissionsSetApproveAllSource = | "user_setting" /** Allow-all was enabled through an RPC caller. */ | "rpc"; +/** + * Where completed plugin content was staged before atomic promotion. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "PluginInstallStagingMode". + */ +/** @experimental */ +export type PluginInstallStagingMode = + /** A sibling of the installed-plugins root, outside the recursively watched tree. */ + | "external" + /** A sibling of the destination plugin directory, used when external staging is unavailable. */ + | "destination_sibling"; /** * Optional flags controlling which side effects the reload performs. * @@ -5197,6 +5223,75 @@ export interface AuthValidationError { */ githubMessage?: string; } +/** + * Current per-window credit limit and consumption for an autopilot objective. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AutopilotObjectiveCreditLimit". + */ +/** @experimental */ +export interface AutopilotObjectiveCreditLimit { + /** + * Configured AI-credit cap, when one is set. + */ + credits?: number; + /** + * Window consumption in fractional AI credits, for display. + */ + creditsUsed: number; + /** + * Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + */ + creditsUsedNanoAiu: string; +} +/** + * Canonical runtime state for the session's current autopilot objective. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AutopilotObjectiveGetStateResult". + */ +/** @experimental */ +export interface AutopilotObjectiveGetStateResult { + /** + * Current objective state, or `null` when the session has no objective. + */ + state: AutopilotObjectiveState | null; +} +/** + * Public, persistence-independent projection of an autopilot objective. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AutopilotObjectiveState". + */ +/** @experimental */ +export interface AutopilotObjectiveState { + /** + * Session-local objective identifier. + */ + id: number; + /** + * User-provided objective text. + */ + objective: string; + status: AutopilotObjectiveStatus; + /** + * Number of objective turns started. + */ + turnCount: number; + /** + * Optional reason the objective is paused. + */ + pauseReason?: string; + /** + * Optional summary recorded when the objective completed. + */ + completionSummary?: string; + /** + * Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. + */ + creditCountNanoAiu: string; + creditLimit?: AutopilotObjectiveCreditLimit; +} /** * The running runtime's complete catalog of well-known built-in model IDs, including supported models and additional IDs with built-in metadata. * @@ -12002,6 +12097,7 @@ export interface McpServer { * Error message if the server failed to connect */ error?: string; + serverMetadata?: McpServerMetadata; } /** * In-process MCP server configuration used by embedded SDK clients. @@ -15026,6 +15122,7 @@ export interface PluginInstallResult { * Number of skills discovered and installed from the plugin */ skillsInstalled: number; + stagingMode?: PluginInstallStagingMode; /** * Optional post-install message provided by the plugin (e.g. setup instructions) */ @@ -20798,6 +20895,7 @@ export interface SlashCommandTimelineEntry { * Optional URL associated with the timeline entry. */ url?: string; + remediation?: RemediationAction; } /** * Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, optional user-facing notice, and settings-change flag. @@ -24977,6 +25075,16 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.workspaces.diff", { sessionId, ...params }), }, /** @experimental */ + autopilotObjective: { + /** + * Reads the current canonical autopilot objective state for this session. + * + * @returns Canonical runtime state for the session's current autopilot objective. + */ + getState: async (): Promise => + connection.sendRequest("session.autopilotObjective.getState", { sessionId }), + }, + /** @experimental */ completions: { /** * Gets the characters that should trigger host-driven completions for the session. Empty disables host-driven completions (e.g. local sessions, or a relay host that does not advertise them). diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index a61a143111..02fbad6c5c 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -187,6 +187,20 @@ export type Verbosity = | "medium" /** A more detailed response was requested. */ | "high"; +/** + * What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. + */ +export type RemediationAction = + /** Authenticate again with the Copilot backend. The current credential is absent, expired, or rejected. */ + | "sign_in" + /** Authenticate as a different account. The current account exists but lacks access to the requested resource. */ + | "switch_account" + /** Inspect which account is currently authenticated before deciding what to change. */ + | "show_account" + /** Review or widen the sandbox policy. The blocked path or host is named by the accompanying message or by the tool result the action arrived with. */ + | "review_sandbox_policy" + /** Permit outbound network access in the sandbox policy. */ + | "allow_sandbox_outbound"; /** * The session mode the agent is operating in */ @@ -1476,6 +1490,7 @@ export interface ErrorData { * GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ providerCallId?: string; + remediation?: RemediationAction; /** * Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation */ @@ -1856,6 +1871,7 @@ export interface WarningData { * Human-readable warning message for display in the timeline */ message: string; + remediation?: RemediationAction; /** * Optional URL associated with this warning that the user can open in a browser */ @@ -6340,6 +6356,7 @@ export interface ToolExecutionCompleteError { * Human-readable error message */ message: string; + remediation?: RemediationAction; } /** * Tool execution result on success @@ -7948,11 +7965,11 @@ export interface PermissionRequestShell { */ possibleUrls: PermissionRequestShellPossibleUrl[]; /** - * True when the model has requested to run this command outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + * True when the tool is asking to run this command outside the sandbox, either because the command detaches and cannot be sandboxed at all, or because a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. */ requestSandboxBypass?: boolean; /** - * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + * What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; /** @@ -8065,11 +8082,11 @@ export interface PermissionRequestRead { */ path: string; /** - * True when the model has requested to run this search outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + * True when the tool is asking to re-run this search outside the sandbox, after a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. */ requestSandboxBypass?: boolean; /** - * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + * What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; /** @@ -8137,11 +8154,11 @@ export interface PermissionRequestUrl { */ redirectedFrom?: string; /** - * True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + * True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. */ requestSandboxBypass?: boolean; /** - * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + * What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; /** @@ -8598,11 +8615,11 @@ export interface PermissionPromptRequestUrl { */ redirectedFrom?: string; /** - * True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + * True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. */ requestSandboxBypass?: boolean; /** - * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + * What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; /** @@ -11197,10 +11214,20 @@ export interface McpServersLoadedServer { * Version of the plugin that supplied the effective MCP server config, only when source is plugin */ pluginVersion?: string; + serverMetadata?: McpServerMetadata; source?: McpServerSource; status: McpServerStatus; transport?: McpServerTransport; } +/** + * Server-advertised metadata learned through modern discovery or legacy initialization. + */ +export interface McpServerMetadata { + /** + * Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. + */ + instructions: string | null; +} /** * Session event "session.mcp_server_status_changed". Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error. */ diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index d14fb4f561..ac4cc5441d 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, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity +from .session_events import AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity if TYPE_CHECKING: from .._jsonrpc import JsonRpcClient @@ -743,6 +743,48 @@ def to_dict(self) -> dict: result["githubMessage"] = from_union([from_str, from_none], self.github_message) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AutopilotObjectiveCreditLimit: + """Current per-window credit limit and consumption for an autopilot objective. + + Current per-window consumption and optional cap, when a credit-tracking window is present. + """ + credits_used: float + """Window consumption in fractional AI credits, for display.""" + + credits_used_nano_aiu: str + """Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string.""" + + credits: float | None = None + """Configured AI-credit cap, when one is set.""" + + @staticmethod + def from_dict(obj: Any) -> 'AutopilotObjectiveCreditLimit': + assert isinstance(obj, dict) + credits_used = from_float(obj.get("creditsUsed")) + credits_used_nano_aiu = from_str(obj.get("creditsUsedNanoAiu")) + credits = from_union([from_float, from_none], obj.get("credits")) + return AutopilotObjectiveCreditLimit(credits_used, credits_used_nano_aiu, credits) + + def to_dict(self) -> dict: + result: dict = {} + result["creditsUsed"] = to_float(self.credits_used) + result["creditsUsedNanoAiu"] = from_str(self.credits_used_nano_aiu) + if self.credits is not None: + result["credits"] = from_union([to_float, from_none], self.credits) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class AutopilotObjectiveStatus(Enum): + """Current normalized lifecycle status. + + Current normalized autopilot objective lifecycle status. + """ + ACTIVE = "active" + COMPLETED = "completed" + PAUSED = "paused" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class BuiltInModelCatalogEntry: @@ -8302,6 +8344,15 @@ def to_dict(self) -> dict: result["version"] = from_union([from_str, from_none], self.version) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class PluginInstallStagingMode(Enum): + """Where the completed plugin tree was staged before atomic promotion + + Where completed plugin content was staged before atomic promotion. + """ + DESTINATION_SIBLING = "destination_sibling" + EXTERNAL = "external" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PluginUpdateResult: @@ -13073,6 +13124,11 @@ class SlashCommandTimelineEntry: type: str """Timeline entry presentation type.""" + remediation: RemediationAction | None = None + """What the user must do to recover, when the entry reports a failure the runtime knows an + action for. The `text` never names a client affordance, so a client that offers one + renders it from this value. + """ url: str | None = None """Optional URL associated with the timeline entry.""" @@ -13081,13 +13137,16 @@ def from_dict(obj: Any) -> 'SlashCommandTimelineEntry': assert isinstance(obj, dict) text = from_str(obj.get("text")) type = from_str(obj.get("type")) + remediation = from_union([RemediationAction, from_none], obj.get("remediation")) url = from_union([from_str, from_none], obj.get("url")) - return SlashCommandTimelineEntry(text, type, url) + return SlashCommandTimelineEntry(text, type, remediation, url) def to_dict(self) -> dict: result: dict = {} result["text"] = from_str(self.text) result["type"] = from_str(self.type) + if self.remediation is not None: + result["remediation"] = from_union([lambda x: to_enum(RemediationAction, x), from_none], self.remediation) if self.url is not None: result["url"] = from_union([from_str, from_none], self.url) return result @@ -15134,6 +15193,64 @@ def to_dict(self) -> dict: result["field"] = from_union([lambda x: to_enum(AgentRegistrySpawnValidationErrorField, x), from_none], self.field) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AutopilotObjectiveState: + """Public, persistence-independent projection of an autopilot objective.""" + + credit_count_nano_aiu: str + """Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a + decimal string. + """ + id: int + """Session-local objective identifier.""" + + objective: str + """User-provided objective text.""" + + status: AutopilotObjectiveStatus + """Current normalized lifecycle status.""" + + turn_count: int + """Number of objective turns started.""" + + completion_summary: str | None = None + """Optional summary recorded when the objective completed.""" + + credit_limit: AutopilotObjectiveCreditLimit | None = None + """Current per-window consumption and optional cap, when a credit-tracking window is present.""" + + pause_reason: str | None = None + """Optional reason the objective is paused.""" + + @staticmethod + def from_dict(obj: Any) -> 'AutopilotObjectiveState': + assert isinstance(obj, dict) + credit_count_nano_aiu = from_str(obj.get("creditCountNanoAiu")) + id = from_int(obj.get("id")) + objective = from_str(obj.get("objective")) + status = AutopilotObjectiveStatus(obj.get("status")) + turn_count = from_int(obj.get("turnCount")) + completion_summary = from_union([from_str, from_none], obj.get("completionSummary")) + credit_limit = from_union([AutopilotObjectiveCreditLimit.from_dict, from_none], obj.get("creditLimit")) + pause_reason = from_union([from_str, from_none], obj.get("pauseReason")) + return AutopilotObjectiveState(credit_count_nano_aiu, id, objective, status, turn_count, completion_summary, credit_limit, pause_reason) + + def to_dict(self) -> dict: + result: dict = {} + result["creditCountNanoAiu"] = from_str(self.credit_count_nano_aiu) + result["id"] = from_int(self.id) + result["objective"] = from_str(self.objective) + result["status"] = to_enum(AutopilotObjectiveStatus, self.status) + result["turnCount"] = from_int(self.turn_count) + if self.completion_summary is not None: + result["completionSummary"] = from_union([from_str, from_none], self.completion_summary) + if self.credit_limit is not None: + result["creditLimit"] = from_union([lambda x: to_class(AutopilotObjectiveCreditLimit, x), from_none], self.credit_limit) + if self.pause_reason is not None: + result["pauseReason"] = from_union([from_str, from_none], self.pause_reason) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class BuiltInModelCatalog: @@ -21950,6 +22067,11 @@ class MCPServer: error: str | None = None """Error message if the server failed to connect""" + server_metadata: McpServerMetadata | None = None + """Server-advertised metadata for a connected server. Omitted when no live connection + metadata is available, including while pending or when failed, disabled, stopped, or not + configured. + """ source: McpServerSource | None = None """Configuration source: user, workspace, plugin, or builtin""" @@ -21965,10 +22087,11 @@ def from_dict(obj: Any) -> 'MCPServer': name = from_str(obj.get("name")) status = McpServerStatus(obj.get("status")) error = from_union([from_str, from_none], obj.get("error")) + server_metadata = from_union([McpServerMetadata.from_dict, from_none], obj.get("serverMetadata")) source = from_union([McpServerSource, from_none], obj.get("source")) source_plugin = from_union([from_str, from_none], obj.get("sourcePlugin")) source_plugin_version = from_union([from_str, from_none], obj.get("sourcePluginVersion")) - return MCPServer(name, status, error, source, source_plugin, source_plugin_version) + return MCPServer(name, status, error, server_metadata, source, source_plugin, source_plugin_version) def to_dict(self) -> dict: result: dict = {} @@ -21976,6 +22099,8 @@ def to_dict(self) -> dict: result["status"] = to_enum(McpServerStatus, self.status) if self.error is not None: result["error"] = from_union([from_str, from_none], self.error) + if self.server_metadata is not None: + result["serverMetadata"] = from_union([lambda x: to_class(McpServerMetadata, x), from_none], self.server_metadata) if self.source is not None: result["source"] = from_union([lambda x: to_enum(McpServerSource, x), from_none], self.source) if self.source_plugin is not None: @@ -26008,6 +26133,25 @@ def to_dict(self) -> dict: result["logCapture"] = from_union([lambda x: to_class(AgentRegistryLogCapture, x), from_none], self.log_capture) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AutopilotObjectiveGetStateResult: + """Canonical runtime state for the session's current autopilot objective.""" + + state: AutopilotObjectiveState | None = None + """Current objective state, or `null` when the session has no objective.""" + + @staticmethod + def from_dict(obj: Any) -> 'AutopilotObjectiveGetStateResult': + assert isinstance(obj, dict) + state = from_union([AutopilotObjectiveState.from_dict, from_none], obj.get("state")) + return AutopilotObjectiveGetStateResult(state) + + def to_dict(self) -> dict: + result: dict = {} + result["state"] = from_union([lambda x: to_class(AutopilotObjectiveState, x), from_none], self.state) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPPlanProvenance: @@ -28319,6 +28463,9 @@ class PluginInstallResult: post_install_message: str | None = None """Optional post-install message provided by the plugin (e.g. setup instructions)""" + staging_mode: PluginInstallStagingMode | None = None + """Where the completed plugin tree was staged before atomic promotion""" + @staticmethod def from_dict(obj: Any) -> 'PluginInstallResult': assert isinstance(obj, dict) @@ -28326,7 +28473,8 @@ def from_dict(obj: Any) -> 'PluginInstallResult': skills_installed = from_int(obj.get("skillsInstalled")) deprecation_warning = from_union([from_str, from_none], obj.get("deprecationWarning")) post_install_message = from_union([from_str, from_none], obj.get("postInstallMessage")) - return PluginInstallResult(plugin, skills_installed, deprecation_warning, post_install_message) + staging_mode = from_union([PluginInstallStagingMode, from_none], obj.get("stagingMode")) + return PluginInstallResult(plugin, skills_installed, deprecation_warning, post_install_message, staging_mode) def to_dict(self) -> dict: result: dict = {} @@ -28336,6 +28484,8 @@ def to_dict(self) -> dict: result["deprecationWarning"] = from_union([from_str, from_none], self.deprecation_warning) if self.post_install_message is not None: result["postInstallMessage"] = from_union([from_str, from_none], self.post_install_message) + if self.staging_mode is not None: + result["stagingMode"] = from_union([lambda x: to_enum(PluginInstallStagingMode, x), from_none], self.staging_mode) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -36387,6 +36537,10 @@ class RPC: auth_info_type: AuthInfoType auth_validation_error: AuthValidationError auth_validation_errors: list[AuthValidationError] + autopilot_objective_credit_limit: AutopilotObjectiveCreditLimit + autopilot_objective_get_state_result: AutopilotObjectiveGetStateResult + autopilot_objective_state: AutopilotObjectiveState + autopilot_objective_status: AutopilotObjectiveStatus built_in_model_catalog: BuiltInModelCatalog built_in_model_catalog_entry: BuiltInModelCatalogEntry builtin_tool_descriptor: BuiltinToolDescriptor @@ -37025,6 +37179,7 @@ class RPC: plan_update_request: PlanUpdateRequest plugin: Plugin plugin_install_result: PluginInstallResult + plugin_install_staging_mode: PluginInstallStagingMode plugin_list: PluginList plugin_list_result: PluginListResult plugins_builtin_set_request: PluginsBuiltinSetRequest @@ -37602,6 +37757,10 @@ def from_dict(obj: Any) -> 'RPC': auth_info_type = AuthInfoType(obj.get("AuthInfoType")) auth_validation_error = AuthValidationError.from_dict(obj.get("AuthValidationError")) auth_validation_errors = from_list(AuthValidationError.from_dict, obj.get("AuthValidationErrors")) + autopilot_objective_credit_limit = AutopilotObjectiveCreditLimit.from_dict(obj.get("AutopilotObjectiveCreditLimit")) + autopilot_objective_get_state_result = AutopilotObjectiveGetStateResult.from_dict(obj.get("AutopilotObjectiveGetStateResult")) + autopilot_objective_state = AutopilotObjectiveState.from_dict(obj.get("AutopilotObjectiveState")) + autopilot_objective_status = AutopilotObjectiveStatus(obj.get("AutopilotObjectiveStatus")) built_in_model_catalog = BuiltInModelCatalog.from_dict(obj.get("BuiltInModelCatalog")) built_in_model_catalog_entry = BuiltInModelCatalogEntry.from_dict(obj.get("BuiltInModelCatalogEntry")) builtin_tool_descriptor = BuiltinToolDescriptor.from_dict(obj.get("BuiltinToolDescriptor")) @@ -38240,6 +38399,7 @@ def from_dict(obj: Any) -> 'RPC': plan_update_request = PlanUpdateRequest.from_dict(obj.get("PlanUpdateRequest")) plugin = Plugin.from_dict(obj.get("Plugin")) plugin_install_result = PluginInstallResult.from_dict(obj.get("PluginInstallResult")) + plugin_install_staging_mode = PluginInstallStagingMode(obj.get("PluginInstallStagingMode")) plugin_list = PluginList.from_dict(obj.get("PluginList")) plugin_list_result = PluginListResult.from_dict(obj.get("PluginListResult")) plugins_builtin_set_request = PluginsBuiltinSetRequest.from_dict(obj.get("PluginsBuiltinSetRequest")) @@ -38764,7 +38924,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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_prune_old_request, sessions_read_persisted_events_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, 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_register_request, tasks_register_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_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, 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, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, 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, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, 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_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, 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, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, 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_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, 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_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, 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, interrupt_main_turn_request, interrupt_main_turn_result, 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, managed_settings_read_result, 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_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, 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_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, 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_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, 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_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, 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_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, 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_apply_startup_overlay_request, model_billing, model_billing_promo, 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_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, 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_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, 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_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, 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_context, 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_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, 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_mode_source, 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_response_capability, 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_mode_request, permissions_get_mode_result, 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_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, 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_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, 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_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, 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, protocol_external_tool_defer, protocol_external_tool_definition, 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_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_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_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, 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_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, 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_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, 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_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_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_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, 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_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, 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_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, 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_prune_old_request, sessions_read_persisted_events_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_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_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_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, 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_register_request, tasks_register_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_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_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_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, 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_agent_metric, 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, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_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, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -38817,6 +38977,10 @@ def to_dict(self) -> dict: result["AuthInfoType"] = to_enum(AuthInfoType, self.auth_info_type) result["AuthValidationError"] = to_class(AuthValidationError, self.auth_validation_error) result["AuthValidationErrors"] = from_list(lambda x: to_class(AuthValidationError, x), self.auth_validation_errors) + result["AutopilotObjectiveCreditLimit"] = to_class(AutopilotObjectiveCreditLimit, self.autopilot_objective_credit_limit) + result["AutopilotObjectiveGetStateResult"] = to_class(AutopilotObjectiveGetStateResult, self.autopilot_objective_get_state_result) + result["AutopilotObjectiveState"] = to_class(AutopilotObjectiveState, self.autopilot_objective_state) + result["AutopilotObjectiveStatus"] = to_enum(AutopilotObjectiveStatus, self.autopilot_objective_status) result["BuiltInModelCatalog"] = to_class(BuiltInModelCatalog, self.built_in_model_catalog) result["BuiltInModelCatalogEntry"] = to_class(BuiltInModelCatalogEntry, self.built_in_model_catalog_entry) result["BuiltinToolDescriptor"] = to_class(BuiltinToolDescriptor, self.builtin_tool_descriptor) @@ -39455,6 +39619,7 @@ def to_dict(self) -> dict: result["PlanUpdateRequest"] = to_class(PlanUpdateRequest, self.plan_update_request) result["Plugin"] = to_class(Plugin, self.plugin) result["PluginInstallResult"] = to_class(PluginInstallResult, self.plugin_install_result) + result["PluginInstallStagingMode"] = to_enum(PluginInstallStagingMode, self.plugin_install_staging_mode) result["PluginList"] = to_class(PluginList, self.plugin_list) result["PluginListResult"] = to_class(PluginListResult, self.plugin_list_result) result["PluginsBuiltinSetRequest"] = to_class(PluginsBuiltinSetRequest, self.plugins_builtin_set_request) @@ -41429,6 +41594,17 @@ async def diff(self, params: WorkspacesDiffRequest, *, timeout: float | None = N return WorkspaceDiffResult.from_dict(await self._client.request("session.workspaces.diff", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class AutopilotObjectiveApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def get_state(self, *, timeout: float | None = None) -> AutopilotObjectiveGetStateResult: + "Reads the current canonical autopilot objective state for this session.\n\nReturns:\n Canonical runtime state for the session's current autopilot objective." + return AutopilotObjectiveGetStateResult.from_dict(await self._client.request("session.autopilotObjective.getState", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class CompletionsApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -42586,6 +42762,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.name = NameApi(client, session_id) self.plan = PlanApi(client, session_id) self.workspaces = WorkspacesApi(client, session_id) + self.autopilot_objective = AutopilotObjectiveApi(client, session_id) self.completions = CompletionsApi(client, session_id) self.instructions = InstructionsApi(client, session_id) self.fleet = FleetApi(client, session_id) @@ -43323,6 +43500,11 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "AuthInfoType", "AuthValidationError", "AuthValidationErrors", + "AutopilotObjectiveApi", + "AutopilotObjectiveCreditLimit", + "AutopilotObjectiveGetStateResult", + "AutopilotObjectiveState", + "AutopilotObjectiveStatus", "BuiltInModelCatalog", "BuiltInModelCatalogEntry", "BuiltinToolDescriptor", @@ -44087,6 +44269,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "PlanUpdateRequest", "Plugin", "PluginInstallResult", + "PluginInstallStagingMode", "PluginList", "PluginListResult", "PluginUpdateAllEntry", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 636f63bcda..52f1053a1f 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -5356,6 +5356,25 @@ def to_dict(self) -> dict: return result +@dataclass +class McpServerMetadata: + "Server-advertised metadata learned through modern discovery or legacy initialization." + instructions: str | None + + @staticmethod + def from_dict(obj: Any) -> "McpServerMetadata": + assert isinstance(obj, dict) + instructions = from_union([from_none, from_str], obj.get("instructions")) + return McpServerMetadata( + instructions=instructions, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["instructions"] = from_union([from_none, from_str], self.instructions) + return result + + @dataclass class McpServersLoadedServer: "A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata." @@ -5364,6 +5383,7 @@ class McpServersLoadedServer: error: str | None = None plugin_name: str | None = None plugin_version: str | None = None + server_metadata: McpServerMetadata | None = None source: McpServerSource | None = None transport: McpServerTransport | None = None @@ -5375,6 +5395,7 @@ def from_dict(obj: Any) -> "McpServersLoadedServer": error = from_union([from_none, from_str], obj.get("error")) plugin_name = from_union([from_none, from_str], obj.get("pluginName")) plugin_version = from_union([from_none, from_str], obj.get("pluginVersion")) + server_metadata = from_union([from_none, McpServerMetadata.from_dict], obj.get("serverMetadata")) source = from_union([from_none, lambda x: parse_enum(McpServerSource, x)], obj.get("source")) transport = from_union([from_none, lambda x: parse_enum(McpServerTransport, x)], obj.get("transport")) return McpServersLoadedServer( @@ -5383,6 +5404,7 @@ def from_dict(obj: Any) -> "McpServersLoadedServer": error=error, plugin_name=plugin_name, plugin_version=plugin_version, + server_metadata=server_metadata, source=source, transport=transport, ) @@ -5397,6 +5419,8 @@ def to_dict(self) -> dict: result["pluginName"] = from_union([from_none, from_str], self.plugin_name) if self.plugin_version is not None: result["pluginVersion"] = from_union([from_none, from_str], self.plugin_version) + if self.server_metadata is not None: + result["serverMetadata"] = from_union([from_none, lambda x: to_class(McpServerMetadata, x)], self.server_metadata) if self.source is not None: result["source"] = from_union([from_none, lambda x: to_enum(McpServerSource, x)], self.source) if self.transport is not None: @@ -8027,6 +8051,7 @@ class SessionErrorData: eligible_for_auto_switch: bool | None = None error_code: str | None = None provider_call_id: str | None = None + remediation: RemediationAction | None = None service_request_id: str | None = None stack: str | None = None status_code: int | None = None @@ -8040,6 +8065,7 @@ def from_dict(obj: Any) -> "SessionErrorData": eligible_for_auto_switch = from_union([from_none, from_bool], obj.get("eligibleForAutoSwitch")) error_code = from_union([from_none, from_str], obj.get("errorCode")) provider_call_id = from_union([from_none, from_str], obj.get("providerCallId")) + remediation = from_union([from_none, lambda x: parse_enum(RemediationAction, x)], obj.get("remediation")) service_request_id = from_union([from_none, from_str], obj.get("serviceRequestId")) stack = from_union([from_none, from_str], obj.get("stack")) status_code = from_union([from_none, from_int], obj.get("statusCode")) @@ -8050,6 +8076,7 @@ def from_dict(obj: Any) -> "SessionErrorData": eligible_for_auto_switch=eligible_for_auto_switch, error_code=error_code, provider_call_id=provider_call_id, + remediation=remediation, service_request_id=service_request_id, stack=stack, status_code=status_code, @@ -8066,6 +8093,8 @@ def to_dict(self) -> dict: result["errorCode"] = from_union([from_none, from_str], self.error_code) if self.provider_call_id is not None: result["providerCallId"] = from_union([from_none, from_str], self.provider_call_id) + if self.remediation is not None: + result["remediation"] = from_union([from_none, lambda x: to_enum(RemediationAction, x)], self.remediation) if self.service_request_id is not None: result["serviceRequestId"] = from_union([from_none, from_str], self.service_request_id) if self.stack is not None: @@ -9236,6 +9265,7 @@ class SessionWarningData: "Warning message for timeline display with categorization" message: str warning_type: str + remediation: RemediationAction | None = None url: str | None = None @staticmethod @@ -9243,10 +9273,12 @@ def from_dict(obj: Any) -> "SessionWarningData": assert isinstance(obj, dict) message = from_str(obj.get("message")) warning_type = from_str(obj.get("warningType")) + remediation = from_union([from_none, lambda x: parse_enum(RemediationAction, x)], obj.get("remediation")) url = from_union([from_none, from_str], obj.get("url")) return SessionWarningData( message=message, warning_type=warning_type, + remediation=remediation, url=url, ) @@ -9254,6 +9286,8 @@ def to_dict(self) -> dict: result: dict = {} result["message"] = from_str(self.message) result["warningType"] = from_str(self.warning_type) + if self.remediation is not None: + result["remediation"] = from_union([from_none, lambda x: to_enum(RemediationAction, x)], self.remediation) if self.url is not None: result["url"] = from_union([from_none, from_str], self.url) return result @@ -10582,15 +10616,18 @@ class ToolExecutionCompleteError: "Error details when the tool execution failed" message: str code: str | None = None + remediation: RemediationAction | None = None @staticmethod def from_dict(obj: Any) -> "ToolExecutionCompleteError": assert isinstance(obj, dict) message = from_str(obj.get("message")) code = from_union([from_none, from_str], obj.get("code")) + remediation = from_union([from_none, lambda x: parse_enum(RemediationAction, x)], obj.get("remediation")) return ToolExecutionCompleteError( message=message, code=code, + remediation=remediation, ) def to_dict(self) -> dict: @@ -10598,6 +10635,8 @@ def to_dict(self) -> dict: result["message"] = from_str(self.message) if self.code is not None: result["code"] = from_union([from_none, from_str], self.code) + if self.remediation is not None: + result["remediation"] = from_union([from_none, lambda x: to_enum(RemediationAction, x)], self.remediation) return result @@ -12560,6 +12599,20 @@ class ReasoningSummary(Enum): DETAILED = "detailed" +class RemediationAction(Enum): + "What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take." + # Authenticate again with the Copilot backend. The current credential is absent, expired, or rejected. + SIGN_IN = "sign_in" + # Authenticate as a different account. The current account exists but lacks access to the requested resource. + SWITCH_ACCOUNT = "switch_account" + # Inspect which account is currently authenticated before deciding what to change. + SHOW_ACCOUNT = "show_account" + # Review or widen the sandbox policy. The blocked path or host is named by the accompanying message or by the tool result the action arrived with. + REVIEW_SANDBOX_POLICY = "review_sandbox_policy" + # Permit outbound network access in the sandbox policy. + ALLOW_SANDBOX_OUTBOUND = "allow_sandbox_outbound" + + class ScheduleOrigin(Enum): "Who created the schedule: `user` (an explicit user action such as `/every` or `/after`) or `model` (the agent via the `manage_schedule` tool). Gates whether a scheduled skill that opted out of model invocation may fire: only user-created schedules may." # The schedule was created by an explicit user action, such as `/every` or `/after`. @@ -13095,6 +13148,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "McpOauthWWWAuthenticateParams", "McpPromptsListChangedData", "McpResourcesListChangedData", + "McpServerMetadata", "McpServerSource", "McpServerStatus", "McpServerTransport", @@ -13171,6 +13225,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "PromptCacheBreakData", "RawSessionEventData", "ReasoningSummary", + "RemediationAction", "SamplingCompletedData", "SamplingRequestedData", "SandboxDecisionData", diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index f1568a8232..1bd83a50f7 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -11,10 +11,10 @@ use serde::{Deserialize, Serialize}; use super::session_events::{ AbortReason, AgentModelPolicy, AutoTier, ContextTier, McpOauthHttpResponse, - McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, - OmittedBinaryOmittedReason, PermissionMode, PermissionPromptRequest, PermissionRule, - ReasoningSummary, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, - TaskCompletionOutcome, UserToolSessionApproval, Verbosity, + McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, + ModelChangeSource, OmittedBinaryOmittedReason, PermissionMode, PermissionPromptRequest, + PermissionRule, ReasoningSummary, RemediationAction, SessionLimitsConfig, SessionMode, + ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -353,6 +353,8 @@ pub mod rpc_methods { pub const SESSION_WORKSPACES_SAVELARGEPASTE: &str = "session.workspaces.saveLargePaste"; /// `session.workspaces.diff` pub const SESSION_WORKSPACES_DIFF: &str = "session.workspaces.diff"; + /// `session.autopilotObjective.getState` + pub const SESSION_AUTOPILOTOBJECTIVE_GETSTATE: &str = "session.autopilotObjective.getState"; /// `session.completions.getTriggerCharacters` pub const SESSION_COMPLETIONS_GETTRIGGERCHARACTERS: &str = "session.completions.getTriggerCharacters"; @@ -2516,6 +2518,73 @@ pub struct AuthValidationError { pub message: String, } +/// Current per-window credit limit and consumption for an autopilot objective. +/// +///
+/// +/// **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 AutopilotObjectiveCreditLimit { + /// Configured AI-credit cap, when one is set. + #[serde(skip_serializing_if = "Option::is_none")] + pub credits: Option, + /// Window consumption in fractional AI credits, for display. + pub credits_used: f64, + /// Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + pub credits_used_nano_aiu: String, +} + +/// Public, persistence-independent projection of an autopilot objective. +/// +///
+/// +/// **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 AutopilotObjectiveState { + /// Optional summary recorded when the objective completed. + #[serde(skip_serializing_if = "Option::is_none")] + pub completion_summary: Option, + /// Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. + pub credit_count_nano_aiu: String, + /// Current per-window consumption and optional cap, when a credit-tracking window is present. + #[serde(skip_serializing_if = "Option::is_none")] + pub credit_limit: Option, + /// Session-local objective identifier. + pub id: i64, + /// User-provided objective text. + pub objective: String, + /// Optional reason the objective is paused. + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_reason: Option, + /// Current normalized lifecycle status. + pub status: AutopilotObjectiveStatus, + /// Number of objective turns started. + pub turn_count: i64, +} + +/// Canonical runtime state for the session's current autopilot objective. +/// +///
+/// +/// **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 AutopilotObjectiveGetStateResult { + /// Current objective state, or `null` when the session has no objective. + pub state: Option, +} + /// A well-known model in the runtime's built-in catalog. /// ///
@@ -9217,6 +9286,9 @@ pub struct McpServer { pub error: Option, /// Server name (config key) pub name: String, + /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + #[serde(skip_serializing_if = "Option::is_none")] + pub server_metadata: Option, /// Configuration source: user, workspace, plugin, or builtin #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, @@ -12852,6 +12924,9 @@ pub struct PluginInstallResult { pub post_install_message: Option, /// Number of skills discovered and installed from the plugin pub skills_installed: i64, + /// Where the completed plugin tree was staged before atomic promotion + #[serde(skip_serializing_if = "Option::is_none")] + pub staging_mode: Option, } /// Plugins installed for the session, with their enabled state and version metadata. @@ -19098,6 +19173,9 @@ pub struct SkillsLoadDiagnostics { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SlashCommandTimelineEntry { + /// What the user must do to recover, when the entry reports a failure the runtime knows an action for. The `text` never names a client affordance, so a client that offers one renders it from this value. + #[serde(skip_serializing_if = "Option::is_none")] + pub remediation: Option, /// Text displayed for the timeline entry. pub text: String, /// Timeline entry presentation type. @@ -22123,6 +22201,9 @@ pub struct PluginsInstallResult { pub post_install_message: Option, /// Number of skills discovered and installed from the plugin pub skills_installed: i64, + /// Where the completed plugin tree was staged before atomic promotion + #[serde(skip_serializing_if = "Option::is_none")] + pub staging_mode: Option, } /// Result of updating a single plugin. @@ -24353,6 +24434,36 @@ pub struct SessionWorkspacesDiffResult { pub unavailable_reason: Option, } +/// Identifies the target session. +/// +///
+/// +/// **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 SessionAutopilotObjectiveGetStateParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Canonical runtime state for the session's current autopilot objective. +/// +///
+/// +/// **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 SessionAutopilotObjectiveGetStateResult { + /// Current objective state, or `null` when the session has no objective. + pub state: Option, +} + /// Identifies the target session. /// ///
@@ -28600,6 +28711,31 @@ pub enum AuthInfoType { Unknown, } +/// Current normalized autopilot objective lifecycle status. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum AutopilotObjectiveStatus { + /// The objective is actively running. + #[serde(rename = "active")] + Active, + /// The objective is paused and may be resumed. + #[serde(rename = "paused")] + Paused, + /// The objective completed. + #[serde(rename = "completed")] + Completed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Custom input-format kind. /// ///
@@ -32682,6 +32818,28 @@ pub enum PermissionsSetApproveAllSource { Unknown, } +/// Where completed plugin content was staged before atomic promotion. +/// +///
+/// +/// **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, PartialEq, Eq, Serialize, Deserialize)] +pub enum PluginInstallStagingMode { + /// A sibling of the installed-plugins root, outside the recursively watched tree. + #[serde(rename = "external")] + External, + /// A sibling of the destination plugin directory, used when external staging is unavailable. + #[serde(rename = "destination_sibling")] + DestinationSibling, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Controls whether the runtime may defer loading an external tool definition. /// ///
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 9dd8f9e58b..50eee0f1fb 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -3100,6 +3100,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.autopilotObjective.*` sub-namespace. + pub fn autopilot_objective(&self) -> SessionRpcAutopilotObjective<'a> { + SessionRpcAutopilotObjective { + session: self.session, + } + } + /// `session.canvas.*` sub-namespace. pub fn canvas(&self) -> SessionRpcCanvas<'a> { SessionRpcCanvas { @@ -3822,6 +3829,42 @@ impl<'a> SessionRpcAgent<'a> { } } +/// `session.autopilotObjective.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcAutopilotObjective<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcAutopilotObjective<'a> { + /// Reads the current canonical autopilot objective state for this session. + /// + /// Wire method: `session.autopilotObjective.getState`. + /// + /// # Returns + /// + /// Canonical runtime state for the session's current autopilot objective. + /// + ///
+ /// + /// **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 get_state(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_AUTOPILOTOBJECTIVE_GETSTATE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.canvas.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcCanvas<'a> { diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index fff10bd697..1d4cdc7e43 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -1110,6 +1110,9 @@ pub struct SessionErrorData { /// GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs #[serde(skip_serializing_if = "Option::is_none")] pub provider_call_id: Option, + /// What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + #[serde(skip_serializing_if = "Option::is_none")] + pub remediation: Option, /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation #[serde(skip_serializing_if = "Option::is_none")] pub service_request_id: Option, @@ -1232,6 +1235,9 @@ pub struct SessionInfoData { pub struct SessionWarningData { /// Human-readable warning message for display in the timeline pub message: String, + /// What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + #[serde(skip_serializing_if = "Option::is_none")] + pub remediation: Option, /// Optional URL associated with this warning that the user can open in a browser #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, @@ -3535,6 +3541,9 @@ pub struct ToolExecutionCompleteError { pub code: Option, /// Human-readable error message pub message: String, + /// What the user must do to recover, when the runtime knows of an action. Set on sandbox policy denials, where `message` names the rule that blocked the call but never the client affordance that relaxes it. + #[serde(skip_serializing_if = "Option::is_none")] + pub remediation: Option, } /// Binary result returned by a tool for the model @@ -4427,10 +4436,10 @@ pub struct PermissionRequestShell { pub possible_paths: Vec, /// URLs that may be accessed by the command pub possible_urls: Vec, - /// True when the model has requested to run this command outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this command outside the sandbox, either because the command detaches and cannot be sandboxed at all, or because a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass: Option, - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request @@ -4485,10 +4494,10 @@ pub struct PermissionRequestRead { pub managed_approval_required: Option, /// Path of the file or directory being read pub path: String, - /// True when the model has requested to run this search outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to re-run this search outside the sandbox, after a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass: Option, - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request @@ -4545,10 +4554,10 @@ pub struct PermissionRequestUrl { /// Immediately preceding URL when this request is for a redirect target #[serde(skip_serializing_if = "Option::is_none")] pub redirected_from: Option, - /// True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass: Option, - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request @@ -4956,10 +4965,10 @@ pub struct PermissionPromptRequestUrl { /// Immediately preceding URL when this prompt is for a redirect target #[serde(skip_serializing_if = "Option::is_none")] pub redirected_from: Option, - /// True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass: Option, - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request @@ -6223,6 +6232,14 @@ pub struct SessionCustomAgentsUpdatedData { pub warnings: Vec, } +/// Server-advertised metadata learned through modern discovery or legacy initialization. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServerMetadata { + /// Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. + pub instructions: Option, +} + /// A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -6238,6 +6255,9 @@ pub struct McpServersLoadedServer { /// Version of the plugin that supplied the effective MCP server config, only when source is plugin #[serde(skip_serializing_if = "Option::is_none")] pub plugin_version: Option, + /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + #[serde(skip_serializing_if = "Option::is_none")] + pub server_metadata: Option, /// Configuration source: user, workspace, plugin, or builtin #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, @@ -6666,6 +6686,30 @@ pub enum Verbosity { Unknown, } +/// What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum RemediationAction { + /// Authenticate again with the Copilot backend. The current credential is absent, expired, or rejected. + #[serde(rename = "sign_in")] + SignIn, + /// Authenticate as a different account. The current account exists but lacks access to the requested resource. + #[serde(rename = "switch_account")] + SwitchAccount, + /// Inspect which account is currently authenticated before deciding what to change. + #[serde(rename = "show_account")] + ShowAccount, + /// Review or widen the sandbox policy. The blocked path or host is named by the accompanying message or by the tool result the action arrived with. + #[serde(rename = "review_sandbox_policy")] + ReviewSandboxPolicy, + /// Permit outbound network access in the sandbox policy. + #[serde(rename = "allow_sandbox_outbound")] + AllowSandboxOutbound, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The session mode the agent is operating in #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SessionMode { From fc4bec5ca60ef286b0558efe6655c4b9c27b2b04 Mon Sep 17 00:00:00 2001 From: Rince Yuan Date: Fri, 4 Sep 2026 19:26:06 +0800 Subject: [PATCH 54/60] docs: fix six broken hook anchor links (#2519) session-lifecycle.md was the only file in docs/ using the explicit heading-id extension (## Heading {#id}). GitHub's Markdown does not implement it, so the braces render as literal text in the heading and the six links that target those ids resolve to nothing. Drop the explicit ids and point the links at the generated slugs, matching the other 50 files in docs/. Co-authored-by: Zhangyi Yuan --- docs/features/hooks.md | 4 ++-- docs/hooks/hooks-overview.md | 8 ++++---- docs/hooks/session-lifecycle.md | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/features/hooks.md b/docs/features/hooks.md index 6a78339901..a3cfc0f503 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -22,13 +22,13 @@ flowchart LR | Hook | When it fires | What you can do | | ------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------ | -| [`onSessionStart`](../hooks/session-lifecycle.md#session-start) | Session begins (new or resumed) | Inject context, load preferences | +| [`onSessionStart`](../hooks/session-lifecycle.md#session-start-hook) | Session begins (new or resumed) | Inject context, load preferences | | [`onUserPromptSubmitted`](../hooks/user-prompt-submitted.md) | User sends a message | Rewrite prompts, add context, filter input | | [`onUserPromptTransformed`](../hooks/user-prompt-transformed.md) | Runtime builds the model prompt | Inspect or replace model-facing content | | [`onPreToolUse`](../hooks/pre-tool-use.md) | Before a tool executes | Allow / deny / modify the call | | [`onPostToolUse`](../hooks/post-tool-use.md) | After a tool returns (success only) | Transform results, redact secrets, audit | | [`onPostToolUseFailure`](../hooks/post-tool-use.md#failure-variant) | After a tool returns a failure | Inject retry guidance, log failures | -| [`onSessionEnd`](../hooks/session-lifecycle.md#session-end) | Session ends | Clean up, record metrics | +| [`onSessionEnd`](../hooks/session-lifecycle.md#session-end-hook) | Session ends | Clean up, record metrics | | [`onErrorOccurred`](../hooks/error-handling.md) | An error is raised | Custom logging, retry logic, alerts | All hooks are **optional**—register only the ones you need. Returning `null` (or the language equivalent) from any hook tells the SDK to continue with default behavior. diff --git a/docs/hooks/hooks-overview.md b/docs/hooks/hooks-overview.md index 8d5583e996..6177d1d2c9 100644 --- a/docs/hooks/hooks-overview.md +++ b/docs/hooks/hooks-overview.md @@ -17,10 +17,10 @@ Hooks allow you to intercept and customize the behavior of Copilot sessions at k | [`onPostToolUseFailure`](./post-tool-use.md#failure-variant) | After a tool execution whose result was a failure | Inject retry guidance, log failures | | [`onUserPromptSubmitted`](./user-prompt-submitted.md) | When user sends a message | Prompt modification, filtering | | [`onUserPromptTransformed`](./user-prompt-transformed.md) | After runtime prompt transformation | Inspect or replace model-facing content | -| [`onSessionStart`](./session-lifecycle.md#session-start) | Session begins | Add context, configure session | -| [`onSessionEnd`](./session-lifecycle.md#session-end) | Session ends | Cleanup, analytics | +| [`onSessionStart`](./session-lifecycle.md#session-start-hook) | Session begins | Add context, configure session | +| [`onSessionEnd`](./session-lifecycle.md#session-end-hook) | Session ends | Cleanup, analytics | | [`onErrorOccurred`](./error-handling.md) | Error happens | Custom error handling | -| [`onAgentStop`](./session-lifecycle.md#agent-stop) | Top-level agent naturally stops | Validate completion or request another turn | +| [`onAgentStop`](./session-lifecycle.md#agent-stop-hook) | Top-level agent naturally stops | Validate completion or request another turn | ## Quick start @@ -266,7 +266,7 @@ const session = await client.createSession({ * **[User Prompt Submitted Hook](./user-prompt-submitted.md)** - Modify user prompts * **[User Prompt Transformed Hook](./user-prompt-transformed.md)** - Replace model-facing prompts * **[Session Lifecycle Hooks](./session-lifecycle.md)** - Session start and end -* **[Agent Stop Hook](./session-lifecycle.md#agent-stop)** - Validate completion before the agent stops +* **[Agent Stop Hook](./session-lifecycle.md#agent-stop-hook)** - Validate completion before the agent stops * **[Error Handling Hook](./error-handling.md)** - Custom error handling ## See also diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index 485752601d..558a729ae7 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -7,7 +7,7 @@ Session lifecycle hooks let you respond to session start and end events. Use the * Track session metrics and analytics * Configure session behavior dynamically -## Session start hook {#session-start} +## Session start hook The `onSessionStart` hook is called when a session begins (new or resumed). @@ -250,7 +250,7 @@ const session = await client.createSession({ }); ``` -## Session end hook {#session-end} +## Session end hook The `onSessionEnd` hook is called when a session ends. @@ -540,7 +540,7 @@ Session Summary: }); ``` -## Agent stop hook {#agent-stop} +## Agent stop hook The agent stop hook runs when the top-level agent naturally reaches the end of a turn. It is separate from `onSessionEnd`: the session remains active, and the hook can request another agent turn. From bba5abd69d87599f9a0f9d82c7c516f0cdbb3c41 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 4 Sep 2026 07:26:30 -0400 Subject: [PATCH 55/60] Expose Auto tier switching across all six SDKs (#2514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Expose Auto tier switching across all six SDKs The runtime now accepts an Auto routing preference change on a live session through `session.model.switchAutoTier`, and reports the authoritative committed, pending, and activating preferences through `session.model.getCurrent`. Without SDK support, integrators could only choose a tier at session creation or resume and had no way to change it or observe whether a change took effect. Each SDK gains two capabilities: * `setAutoTier` changes the routing preference without changing the selected model. * The model switch options gain an Auto tier field, which stages a tier atomically with selecting `auto`. The runtime distinguishes an explicit null tier, meaning "return to provider-default routing," from an absent one, meaning "leave the preference alone." Python, Go, .NET, Java, and Rust generated wrappers drop null properties, so those SDKs build the request payload directly where a null must survive; each bypass carries a comment explaining why. Node.js needed no workaround. The tri-state choice is expressed idiomatically per language: `undefined`/tier/`null` in Node.js, an `_UNSET` sentinel in Python, a `ClearAutoTier` flag validated as mutually exclusive in Go, .NET, and Java, and an `AutoTierPreference` enum in Rust. Tests cover the wire payloads for both methods, including the explicit null case, and decoding of the ephemeral `session.auto_tier_switch_failed` event across all four failure reasons plus the null requested-tier case. Documentation previously stated that a resident resume rejects a different tier. The authoritative schema now says the runtime requests a safe switch applied after the resume succeeds, which cannot change a turn already in flight. This corrects that text in all six SDKs and in the session persistence guide. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2aad36e9-e078-4b9c-afa1-0c7cd2c71f74 * Address pre-review findings on Auto tier switching Reviewing the change against the standards maintainers have applied to earlier SDK pull requests surfaced four issues worth fixing before opening the pull request. Python could not accept the enum it hands back. `set_auto_tier` and `set_model` were typed for the `AutoTier` string literal, but results and events carry the generated `AutoTier` enum. Feeding that value back raised `TypeError: Object of type AutoTier is not JSON serializable`, because the JSON-RPC encoder calls `json.dumps` with no enum support. Both methods now accept either representation and normalize to the wire value. Added a regression test. .NET and Go hand-copied request fields. Both built a second, partial copy of the model-switch payload so an explicit null tier would survive serialization. Each copy listed only a subset of the generated request's fields, so a new field on the schema would have been silently dropped on the clear path. Both now serialize the generated request and write the null back, matching what Rust, Java, and Python already did. This also removes two hand-written request types and their serializer registrations from the .NET SDK. Java allocated raw `ObjectMapper` instances instead of using the shared `MAPPER`, which is configured to match the JSON-RPC encoder. A differently configured mapper could produce a different wire shape. The .NET test compared the status enum through `ToString`; it now compares the enum directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2aad36e9-e078-4b9c-afa1-0c7cd2c71f74 * Mark the Auto tier surface experimental in every SDK The .NET and Java implementations carried [Experimental] and @CopilotExperimental, but the Node.js, Python, Go, and Rust equivalents carried no marker. That asymmetry is the exact gap maintainers have flagged before: C# gets the annotation and the other languages are left without the corresponding doc annotation. Auto tier routing is still moving — the `fast` tier is not yet exposed and the runtime contract may shift — so the surface is marked experimental consistently in all six SDKs, each using the convention already established in that language. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2aad36e9-e078-4b9c-afa1-0c7cd2c71f74 * Use one vocabulary for resetting the Auto tier The SDKs described the same idea three different ways: Rust called it `AutoTierPreference::ProviderDefault`, while Go, .NET, and Java called it `ClearAutoTier`. Cross-SDK consistency is the standard maintainers apply most often, and a reader comparing two SDKs had no way to tell these were the same operation. Everything now uses "reset": `ResetAutoTier` in Go and .NET, `setResetAutoTier` in Java, and `AutoTierPreference::Reset` with `with_reset_auto_tier` in Rust. The underlying shapes stay language-idiomatic. Node.js and Python express all three states natively, because `null` and `None` are distinguishable from an omitted argument. Rust uses a sum type. Go, .NET, and Java cannot make that distinction in a single value, so they keep a separate reset option. Forcing Rust's enum onto Go would mean exported constructor functions in place of the pointer-and-flag pattern Go already uses for "unset versus explicit zero", which trades a cross-language inconsistency for a within-language one. Documented the trade-off and added a per-SDK table for staging a tier on a model switch, so the difference is explained rather than discovered. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2aad36e9-e078-4b9c-afa1-0c7cd2c71f74 * Add end-to-end coverage for Auto tier switching The unit tests for Auto tier switching only check the JSON-RPC payload the SDK builds. That proves the wire shape but not that the runtime interprets it the way the API documents, which is the part reviewers have asked us to demonstrate on earlier model-switching work. These tests run against a real Copilot runtime and read the staged state back through `model.getCurrent()`, so every assertion reflects recorded runtime behavior. Two scenarios, identical across all six SDKs: - Staging and resetting. A request is accepted as `pending`, a second request replaces the first and reports the tier it displaced, and a null tier returns the session to provider-default routing. - Omission semantics. Calling `setModel("auto")` without a tier preserves the staged preference, passing a tier replaces it, and asking for a reset clears it. These are three distinct outcomes, which is why the reset is expressed separately from the tier value in every language. The runtime only commits a preference on a later turn that uses the `auto` model, so it rejects `switchAutoTier` unless `auto` is selected. Both snapshots therefore list `auto` and record no conversation; covering the commit path through `session.model_change` and `session.auto_tier_switch_failed` needs a recorded conversation and is left as follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2aad36e9-e078-4b9c-afa1-0c7cd2c71f74 * Fix Rust CI: format E2E imports and skip the README fragment The Auto tier README example is a fragment that references an undefined 'session' binding, so rustdoc could not compile it as a doctest. Mark it 'rust,ignore' to match every other fragment in the file, and apply the rustfmt import merge the format job asked for. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2aad36e9-e078-4b9c-afa1-0c7cd2c71f74 * Address review feedback on the Auto tier surface Gate the experimental Auto tier options consistently. The .NET SetModelOptions.AutoTier and .ResetAutoTier properties, the Java Auto tier accessors, and the Python set_model auto_tier parameter now carry the same experimental marker their dedicated setAutoTier counterparts already had. Correct two documentation errors. The reset-state paragraph claimed four SDKs cannot express reset in a single value; Rust can, through AutoTierPreference::Reset, so only Go, .NET, and Java need a separate flag. The live-switching section now states that it needs Copilot CLI 1.0.83-4, which is newer than the 1.0.82-1 required to select a tier at create or resume. Fix the Java class Javadoc, which said every option is optional when setModel rejects a null model, and rewrite the Rust E2E doc comment to describe what it covers instead of pointing at the Node.js test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2aad36e9-e078-4b9c-afa1-0c7cd2c71f74 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2aad36e9-e078-4b9c-afa1-0c7cd2c71f74 --- docs/features/session-persistence.md | 46 +++- dotnet/README.md | 21 ++ dotnet/src/Session.cs | 91 +++++++ dotnet/src/Types.cs | 28 ++- dotnet/test/E2E/AutoTierE2ETests.cs | 88 +++++++ .../test/Unit/ClientSessionLifetimeTests.cs | 114 +++++++++ .../Unit/SessionEventSerializationTests.cs | 61 +++++ go/README.md | 24 ++ go/internal/e2e/auto_tier_e2e_test.go | 140 +++++++++++ go/session.go | 67 +++++ go/session_event_serialization_test.go | 67 +++++ go/session_test.go | 100 +++++++- go/types.go | 10 +- java/README.md | 24 +- .../com/github/copilot/CopilotSession.java | 144 +++++++++-- .../copilot/rpc/CapiSessionOptions.java | 5 +- .../github/copilot/rpc/SetModelOptions.java | 177 ++++++++++++++ .../java/com/github/copilot/AutoTierIT.java | 120 +++++++++ .../copilot/SessionAutoTierEventTest.java | 37 +++ .../copilot/SessionAutoTierSwitchTest.java | 228 ++++++++++++++++++ nodejs/README.md | 26 ++ nodejs/src/index.ts | 3 + nodejs/src/session.ts | 52 ++++ nodejs/src/types.ts | 15 +- nodejs/test/client.test.ts | 111 +++++++++ nodejs/test/e2e/auto_tier.e2e.test.ts | 73 ++++++ nodejs/test/session-event-types.test.ts | 43 ++++ python/README.md | 19 ++ python/copilot/__init__.py | 10 + python/copilot/client.py | 15 +- python/copilot/session.py | 120 ++++++++- python/e2e/test_auto_tier_e2e.py | 81 +++++++ python/test_client.py | 132 ++++++++++ python/test_event_forward_compatibility.py | 49 ++++ rust/README.md | 27 ++- rust/src/session.rs | 78 +++++- rust/src/types.rs | 50 +++- rust/tests/api_types_test.rs | 69 +++++- rust/tests/e2e.rs | 2 + rust/tests/e2e/auto_tier.rs | 140 +++++++++++ ...rve_auto_tier_when_set_model_omits_it.yaml | 4 + ..._stage_and_reset_auto_tier_preference.yaml | 4 + 42 files changed, 2640 insertions(+), 75 deletions(-) create mode 100644 dotnet/test/E2E/AutoTierE2ETests.cs create mode 100644 go/internal/e2e/auto_tier_e2e_test.go create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/SetModelOptions.java create mode 100644 java/sdk/src/test/java/com/github/copilot/AutoTierIT.java create mode 100644 java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java create mode 100644 nodejs/test/e2e/auto_tier.e2e.test.ts create mode 100644 python/e2e/test_auto_tier_e2e.py create mode 100644 rust/tests/e2e/auto_tier.rs create mode 100644 test/snapshots/auto_tier/should_preserve_auto_tier_when_set_model_omits_it.yaml create mode 100644 test/snapshots/auto_tier/should_stage_and_reset_auto_tier_preference.yaml diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 9b0aa9434c..869af9b9a4 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -242,7 +242,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `availableTools` | Restrict which tools are available | | `excludedTools` | Disable specific tools | | `provider` | Re-provide BYOK credentials (required for BYOK sessions) | -| `capi.autoTier` | Override the persisted Auto routing preference on cold resume only | +| `capi.autoTier` | Override the persisted Auto routing preference | | `reasoningEffort` | Adjust reasoning effort level | | `streaming` | Enable/disable streaming responses | | `workingDirectory` | Change the working directory | @@ -262,13 +262,55 @@ The runtime persists the selected tier, so applications do not need to resend it * Omitting the tier when creating a session uses the runtime's default routing behavior. * A cold resume restores the persisted tier. Supplying an explicit tier overrides the restored value for the new activation. -* When resuming a session already resident in the runtime, omitting the tier preserves the current selection, supplying the same tier is a no-op, and supplying a different tier is rejected. +* When resuming a session already resident in the runtime, omitting the tier preserves the current selection and supplying the same tier is a no-op. Supplying a different tier requests a safe switch that the runtime applies after the resume succeeds; it cannot change a turn that is already in flight. * Older sessions without a persisted tier retain default routing behavior. Tier selection is not a live model-switch operation. The SDK forwards the preference; the runtime owns persistence and validation. The `session.start` and `session.resume` events expose the selected tier in their optional `data.autoTier` field (`data.auto_tier` in Python). When no tier is selected, the field is omitted. +### Changing the Auto tier during a session + +Call `setAutoTier` to change the routing preference on a live session without changing the selected model. Pass `null` (Python `None`, Go `nil`) to return to the provider's default Auto routing. This requires Copilot CLI `1.0.83-4` or later, which is newer than the `1.0.82-1` needed to select a tier when creating or resuming a session. + +```typescript +const result = await session.setAutoTier("intelligence"); +if (result.status === "pending") { + // Accepted, but not yet in effect. +} +``` + +The runtime does not apply the preference immediately. It records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider. A `pending` status therefore confirms that the request was accepted, not that it took effect. Only the most recent request survives: a new request replaces any earlier one that no turn has claimed yet. + +Watch for the outcome through these events: + +* `session.model_change` when the preference commits. +* `session.auto_tier_switch_failed` when it does not. This event is ephemeral, so the runtime never persists or replays it on resume. Its `reason` field is one of `policy_rejected`, `request_failed`, `setup_failed`, or `unsupported`, and the previously effective preference stays active. + +You can also read the authoritative state at any time through the session's `model.getCurrent` RPC method, which reports the committed `autoTier`, any unclaimed `pendingAutoTier`, and the `activatingAutoTier` currently claimed by an in-progress activation. + +| SDK | Change the tier | Return to provider-default routing | +|-----|-----------------|------------------------------------| +| Node.js | `session.setAutoTier("balance")` | `session.setAutoTier(null)` | +| Python | `session.set_auto_tier("balance")` | `session.set_auto_tier(None)` | +| Go | `session.SetAutoTier(ctx, &tier)` | `session.SetAutoTier(ctx, nil)` | +| .NET | `session.SetAutoTierAsync(AutoTier.Balance)` | `session.SetAutoTierAsync(null)` | +| Rust | `session.set_auto_tier(Some(AutoTier::Balance))` | `session.set_auto_tier(None)` | +| Java | `session.setAutoTier(AutoTier.BALANCE)` | `session.setAutoTier(null)` | + +To select the `auto` model and its routing preference in a single call, stage the tier on the model switch instead. The runtime rejects this option when the model is anything other than `auto`. + +| SDK | Stage a tier with the switch | Reset to provider-default routing | +|-----|------------------------------|-----------------------------------| +| Node.js | `setModel("auto", { autoTier: "balance" })` | `setModel("auto", { autoTier: null })` | +| Python | `set_model("auto", auto_tier="balance")` | `set_model("auto", auto_tier=None)` | +| Go | `SetModelOptions{AutoTier: &tier}` | `SetModelOptions{ResetAutoTier: true}` | +| .NET | `new SetModelOptions { AutoTier = AutoTier.Balance }` | `new SetModelOptions { ResetAutoTier = true }` | +| Rust | `SetModelOptions::default().with_auto_tier(AutoTier::Balance)` | `SetModelOptions::default().with_reset_auto_tier()` | +| Java | `new SetModelOptions().setModel("auto").setAutoTier(AutoTier.BALANCE)` | `new SetModelOptions().setModel("auto").setResetAutoTier(true)` | + +Node.js, Python, and Rust express all three states in a single value: Node.js and Python because `null`/`None` is distinguishable from an omitted argument, and Rust because `AutoTierPreference::Reset` is a distinct variant of the same option. Go, .NET, and Java have no way to distinguish "reset" from "unset" in one value, so they carry a separate reset flag. Omitting both always means "leave the current preference alone." + ### Example: changing model on resume ```typescript diff --git a/dotnet/README.md b/dotnet/README.md index 23a78030b4..5c058d19a9 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -284,6 +284,27 @@ await session2.DisposeAsync(); --- +## Auto routing tiers + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```csharp +var result = await session.SetAutoTierAsync(AutoTier.Intelligence); +if (result.Status == ModelSwitchAutoTierStatus.Pending) +{ + // Accepted, but not yet in effect. +} + +// Return to the provider's default Auto routing. +await session.SetAutoTierAsync(null); +``` + +`SetModelAsync` accepts the same preference through `SetModelOptions.AutoTier`, which stages the tier atomically with selecting `auto`. Set `ResetAutoTier` instead to return to provider-default routing; the two options are mutually exclusive. + +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. + ## Event Types Sessions emit various events during processing. Each event type is a class that inherits from `SessionEvent`: diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index d531238435..42cbe3f9ce 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -11,6 +11,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Threading.Channels; namespace GitHub.Copilot; @@ -1927,8 +1928,35 @@ public async Task SetModelAsync(string model, SetModelOptions options, Cancellat ArgumentNullException.ThrowIfNull(model); ThrowIfDisposed(); + if (options.AutoTier is not null && options.ResetAutoTier) + { + throw new ArgumentException( + $"{nameof(SetModelOptions.AutoTier)} and {nameof(SetModelOptions.ResetAutoTier)} are mutually exclusive.", + nameof(options)); + } + + if (options.ResetAutoTier) + { + var request = new ModelSwitchToRequest + { + SessionId = SessionId, + ModelId = model, + ReasoningEffort = options.ReasoningEffort, + ReasoningSummary = options.ReasoningSummary, + ModelCapabilities = options.ModelCapabilities, + ContextTier = options.ContextTier, + }; + await CopilotClient.InvokeRpcAsync( + Rpc, + "session.model.switchTo", + [WithExplicitNullAutoTier(request, RpcJsonContext.Default.ModelSwitchToRequest)], + cancellationToken); + return; + } + await Rpc.Model.SwitchToAsync( modelId: model, + autoTier: options.AutoTier, reasoningEffort: options.ReasoningEffort, reasoningSummary: options.ReasoningSummary, verbosity: null, @@ -1938,6 +1966,69 @@ await Rpc.Model.SwitchToAsync( cancellationToken: cancellationToken); } + /// + /// Changes the Auto routing preference without changing the selected model. + /// + /// + /// + /// The runtime does not apply the preference immediately. It records the request and + /// commits it only when a later user turn using the auto model successfully + /// obtains a usable model from the provider. A pending status therefore confirms + /// that the request was accepted, not that it took effect. + /// + /// + /// Watch for the outcome through the session.model_change event on success, or the + /// ephemeral session.auto_tier_switch_failed event on failure. You can also read + /// the current committed and in-flight state at any time with + /// session.Rpc.Model.GetCurrentAsync. + /// + /// + /// Only the most recent request survives: issuing a new request replaces any earlier one + /// that has not yet been claimed by a turn. + /// + /// + /// Routing preference to activate, or to return to the provider's default Auto routing. + /// Optional cancellation token. + /// The runtime's immediate acknowledgement and Auto preference snapshot. + /// + /// + /// var result = await session.SetAutoTierAsync(AutoTier.Intelligence); + /// + /// + [Experimental(Diagnostics.Experimental)] + public async Task SetAutoTierAsync(AutoTier? autoTier, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + if (autoTier is not null) + { + return await Rpc.Model.SwitchAutoTierAsync(autoTier, cancellationToken: cancellationToken); + } + + var request = new ModelSwitchAutoTierRequest { SessionId = SessionId }; + return await CopilotClient.InvokeRpcAsync( + Rpc, + "session.model.switchAutoTier", + [WithExplicitNullAutoTier(request, RpcJsonContext.Default.ModelSwitchAutoTierRequest)], + cancellationToken); + } + + /// + /// Serializes a generated request and restores the autoTier property as an explicit null. + /// + /// + /// The generated request types omit autoTier when it is null. The runtime reads an + /// omitted tier as "leave the current preference alone" and an explicit null as "return to + /// provider-default Auto routing", so the null has to survive serialization. Serializing the + /// generated type keeps every other field on the request in sync with the schema. + /// + private static JsonObject WithExplicitNullAutoTier(T request, JsonTypeInfo typeInfo) + { + var payload = JsonSerializer.SerializeToNode(request, typeInfo)!.AsObject(); + payload["autoTier"] = null; + return payload; + } + /// /// Changes the model for this session. /// diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 3af7ea7f12..9129b118e8 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2447,9 +2447,11 @@ public sealed class CapiSessionOptions ///
/// /// Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto. - /// When omitted, the runtime uses its default on create and preserves the persisted or current - /// tier on resume. An explicit tier overrides the persisted tier on a cold resume; a conflicting - /// tier on a resident session resume is rejected by the runtime. + /// When omitted, the runtime uses its default on create and restores the last committed + /// tier on cold resume. On resident resume, a different tier requests a safe switch that + /// takes effect after resume succeeds and never disturbs a turn that is already running. + /// To change the preference on a live session, use + /// . /// [JsonPropertyName("autoTier")] public AutoTier? AutoTier { get; set; } @@ -3017,6 +3019,26 @@ public struct SetModelOptions /// Per-property overrides for model capabilities, deep-merged over runtime defaults. public ModelCapabilitiesOverride? ModelCapabilities { get; set; } + + /// + /// Routing preference to stage atomically with selecting the auto model. + /// + /// + /// Leave unset to leave the current preference alone. Set + /// instead to return to the provider's default Auto + /// routing. The runtime rejects this option when the model is anything other than + /// auto; use to change the + /// preference without changing the selected model. + /// + [Experimental(Diagnostics.Experimental)] + public AutoTier? AutoTier { get; set; } + + /// + /// Returns to the provider's default Auto routing as part of this switch. + /// Mutually exclusive with . + /// + [Experimental(Diagnostics.Experimental)] + public bool ResetAutoTier { get; set; } } /// diff --git a/dotnet/test/E2E/AutoTierE2ETests.cs b/dotnet/test/E2E/AutoTierE2ETests.cs new file mode 100644 index 0000000000..3758eee0f5 --- /dev/null +++ b/dotnet/test/E2E/AutoTierE2ETests.cs @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.Rpc; +using GitHub.Copilot.Test.Harness; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.Test.E2E; + +/// +/// Mirrors nodejs/test/e2e/auto_tier.e2e.test.ts (snapshot category "auto_tier"). +/// +/// +/// The runtime stages an Auto routing preference instead of applying it immediately: a +/// request stays unclaimed until a later turn using the auto model mints a usable +/// model and token pair. These tests observe that staged state through +/// Model.GetCurrentAsync, so they assert what the runtime actually recorded rather +/// than what the SDK serialized. +/// +public class AutoTierE2ETests(E2ETestFixture fixture, ITestOutputHelper output) + : E2ETestBase(fixture, "auto_tier", output) +{ + private static async Task AssertPendingAutoTierAsync(CopilotSession session, AutoTier? expected) + { + var current = await session.Rpc.Model.GetCurrentAsync(); + Assert.Equal(expected, current.PendingAutoTier); + } + + [Fact] + public async Task Should_Stage_And_Reset_Auto_Tier_Preference() + { + await using var session = await CreateSessionAsync(new SessionConfig + { + Model = "auto", + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + await AssertPendingAutoTierAsync(session, null); + + var staged = await session.SetAutoTierAsync(AutoTier.Efficiency); + Assert.Equal(ModelSwitchAutoTierStatus.Pending, staged.Status); + Assert.Equal(AutoTier.Efficiency, staged.PendingAutoTier); + await AssertPendingAutoTierAsync(session, AutoTier.Efficiency); + + // A second request replaces the first and reports the one it displaced. + var superseded = await session.SetAutoTierAsync(AutoTier.Intelligence); + Assert.Equal(ModelSwitchAutoTierStatus.Pending, superseded.Status); + Assert.Equal(AutoTier.Intelligence, superseded.PendingAutoTier); + Assert.Equal(AutoTier.Efficiency, superseded.SupersededAutoTier); + await AssertPendingAutoTierAsync(session, AutoTier.Intelligence); + + // A null tier returns the session to provider-default routing. The status is + // Unchanged because provider-default was already the committed preference; the + // request's effect is cancelling the staged one. + var reset = await session.SetAutoTierAsync(null); + Assert.Equal(ModelSwitchAutoTierStatus.Unchanged, reset.Status); + Assert.Equal(AutoTier.Intelligence, reset.SupersededAutoTier); + await AssertPendingAutoTierAsync(session, null); + } + + [Fact] + public async Task Should_Preserve_Auto_Tier_When_Set_Model_Omits_It() + { + await using var session = await CreateSessionAsync(new SessionConfig + { + Model = "auto", + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + await session.SetAutoTierAsync(AutoTier.Balance); + await AssertPendingAutoTierAsync(session, AutoTier.Balance); + + // Leaving AutoTier unset without asking for a reset leaves the staged preference alone. + await session.SetModelAsync("auto", new SetModelOptions()); + await AssertPendingAutoTierAsync(session, AutoTier.Balance); + + // Supplying a tier replaces it. + await session.SetModelAsync("auto", new SetModelOptions { AutoTier = AutoTier.Intelligence }); + await AssertPendingAutoTierAsync(session, AutoTier.Intelligence); + + // ResetAutoTier clears it. Omission, a value, and a reset are three distinct + // outcomes, which is why a single nullable property cannot express the request. + await session.SetModelAsync("auto", new SetModelOptions { ResetAutoTier = true }); + await AssertPendingAutoTierAsync(session, null); + } +} diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index 204626a6f5..20c25f4885 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -512,6 +512,111 @@ public async Task SessionRequests_Serialize_CapiAutoTier(AutoTier tier, string e } } + [Theory] + [InlineData("efficiency")] + [InlineData("balance")] + [InlineData("intelligence")] + public async Task SetModelAsync_Serializes_AutoTier(string expectedTier) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await session.SetModelAsync("auto", new SetModelOptions { AutoTier = new AutoTier(expectedTier) }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchTo"); + Assert.Equal("auto", request.Params.GetProperty("modelId").GetString()); + Assert.Equal(expectedTier, request.Params.GetProperty("autoTier").GetString()); + } + + [Fact] + public async Task SetModelAsync_Omits_AutoTier_WhenUnset() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await session.SetModelAsync("gpt-5.4"); + + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchTo"); + Assert.False(request.Params.TryGetProperty("autoTier", out _)); + } + + [Fact] + public async Task SetModelAsync_Writes_Null_AutoTier_WhenCleared() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await session.SetModelAsync("auto", new SetModelOptions { ResetAutoTier = true }); + + // An explicit null must survive to the wire. Omitting it would mean "leave the + // preference alone" rather than "use provider-default routing". + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchTo"); + Assert.True(request.Params.TryGetProperty("autoTier", out var autoTier)); + Assert.Equal(JsonValueKind.Null, autoTier.ValueKind); + } + + [Fact] + public async Task SetModelAsync_Rejects_Conflicting_AutoTier_Options() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await Assert.ThrowsAsync(() => session.SetModelAsync( + "auto", + new SetModelOptions { AutoTier = AutoTier.Balance, ResetAutoTier = true })); + } + + [Fact] + public async Task SetAutoTierAsync_Serializes_Tier_And_Returns_Snapshot() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var result = await session.SetAutoTierAsync(AutoTier.Intelligence); + + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchAutoTier"); + Assert.Equal("intelligence", request.Params.GetProperty("autoTier").GetString()); + Assert.Equal(ModelSwitchAutoTierStatus.Pending, result.Status); + Assert.Equal(AutoTier.Balance, result.EffectiveAutoTier); + } + + [Fact] + public async Task SetAutoTierAsync_Writes_Null_Tier_ForDefaultRouting() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await session.SetAutoTierAsync(null); + + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchAutoTier"); + Assert.True(request.Params.TryGetProperty("autoTier", out var autoTier)); + Assert.Equal(JsonValueKind.Null, autoTier.ValueKind); + } + [Theory] [InlineData(false, null)] [InlineData(true, null)] @@ -1832,6 +1937,15 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel { ["success"] = true }, + "session.model.switchTo" => new Dictionary + { + ["modelId"] = "auto" + }, + "session.model.switchAutoTier" => new Dictionary + { + ["status"] = "pending", + ["effectiveAutoTier"] = "balance" + }, "session.delete" => new Dictionary { ["success"] = true diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 25c56838bc..405ffb379f 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -82,6 +82,67 @@ public void UserMessageEvent_MessageId_UsesCamelCaseAndIsOptional(string? messag } } + public static TheoryData AutoTierSwitchFailureReasons => + [ + "policy_rejected", + "request_failed", + "setup_failed", + "unsupported", + ]; + + [Theory] + [MemberData(nameof(AutoTierSwitchFailureReasons))] + public void SessionEvent_Deserializes_AutoTierSwitchFailed(string wireReason) + { + var json = $$""" + { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": "session.auto_tier_switch_failed", + "data": { + "effectiveAutoTier": "balance", + "requestedAutoTier": "intelligence", + "reason": "{{wireReason}}" + } + } + """; + + var sessionEvent = SessionEvent.FromJson(json); + + var data = Assert.IsType(sessionEvent).Data; + Assert.Equal(new AutoTierSwitchFailureReason(wireReason), data.Reason); + Assert.Equal(AutoTier.Balance, data.EffectiveAutoTier); + Assert.Equal(AutoTier.Intelligence, data.RequestedAutoTier); + } + + [Fact] + public void SessionEvent_Deserializes_AutoTierSwitchFailed_WithNullRequestedTier() + { + // A null requested tier means the attempt to return to provider-default + // Auto routing is what failed. + var json = """ + { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": "session.auto_tier_switch_failed", + "data": { + "effectiveAutoTier": "efficiency", + "requestedAutoTier": null, + "reason": "unsupported" + } + } + """; + + var sessionEvent = SessionEvent.FromJson(json); + + var data = Assert.IsType(sessionEvent).Data; + Assert.Null(data.RequestedAutoTier); + Assert.Equal(AutoTier.Efficiency, data.EffectiveAutoTier); + Assert.Equal(AutoTierSwitchFailureReason.Unsupported, data.Reason); + } + public static TheoryData JsonElementBackedEvents => new() { { diff --git a/go/README.md b/go/README.md index 801ad8556c..4f31a0e0b9 100644 --- a/go/README.md +++ b/go/README.md @@ -332,6 +332,30 @@ Each section override supports five actions: Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored. +## Auto routing tiers + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```go +tier := copilot.AutoTierIntelligence +result, err := session.SetAutoTier(ctx, &tier) +if err != nil { + return err +} +if result.Status == rpc.ModelSwitchAutoTierStatusPending { + // Accepted, but not yet in effect. +} + +// Return to the provider's default Auto routing. +_, err = session.SetAutoTier(ctx, nil) +``` + +`SetModel` accepts the same preference through `SetModelOptions.AutoTier`, which stages the tier atomically with selecting `auto`. Set `ResetAutoTier` instead to return to provider-default routing; the two options are mutually exclusive. + +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. + ## Image Support The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: diff --git a/go/internal/e2e/auto_tier_e2e_test.go b/go/internal/e2e/auto_tier_e2e_test.go new file mode 100644 index 0000000000..e974f95927 --- /dev/null +++ b/go/internal/e2e/auto_tier_e2e_test.go @@ -0,0 +1,140 @@ +package e2e + +import ( + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" +) + +// Mirrors nodejs/test/e2e/auto_tier.e2e.test.ts (snapshot category "auto_tier"). +// +// The runtime stages an Auto routing preference instead of applying it immediately: a +// request stays unclaimed until a later turn using the "auto" model mints a usable +// model and token pair. These tests observe that staged state through Model.GetCurrent, +// so they assert what the runtime actually recorded rather than what the SDK serialized. +func TestAutoTierE2E(t *testing.T) { + autoTier := func(tier copilot.AutoTier) *copilot.AutoTier { return &tier } + + pendingTier := func(t *testing.T, session *copilot.Session) *rpc.AutoTier { + t.Helper() + current, err := session.RPC.Model.GetCurrent(t.Context()) + if err != nil { + t.Fatalf("Model.GetCurrent failed: %v", err) + } + return current.PendingAutoTier + } + + assertPending := func(t *testing.T, session *copilot.Session, want rpc.AutoTier) { + t.Helper() + got := pendingTier(t, session) + if got == nil || *got != want { + t.Fatalf("Expected pending auto tier %q, got %v", want, got) + } + } + + assertNoPending := func(t *testing.T, session *copilot.Session) { + t.Helper() + if got := pendingTier(t, session); got != nil { + t.Fatalf("Expected no pending auto tier, got %q", *got) + } + } + + newAutoSession := func(t *testing.T) *copilot.Session { + t.Helper() + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + ctx.ConfigureForTest(t) + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + Model: "auto", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + return session + } + + t.Run("should stage and reset auto tier preference", func(t *testing.T) { + session := newAutoSession(t) + assertNoPending(t, session) + + staged, err := session.SetAutoTier(t.Context(), autoTier(copilot.AutoTierEfficiency)) + if err != nil { + t.Fatalf("SetAutoTier(efficiency) failed: %v", err) + } + if staged.Status != rpc.ModelSwitchAutoTierStatusPending { + t.Fatalf("Expected status pending, got %q", staged.Status) + } + if staged.PendingAutoTier == nil || *staged.PendingAutoTier != rpc.AutoTierEfficiency { + t.Fatalf("Expected pending efficiency in result, got %+v", staged) + } + assertPending(t, session, rpc.AutoTierEfficiency) + + // A second request replaces the first and reports the one it displaced. + superseded, err := session.SetAutoTier(t.Context(), autoTier(copilot.AutoTierIntelligence)) + if err != nil { + t.Fatalf("SetAutoTier(intelligence) failed: %v", err) + } + if superseded.Status != rpc.ModelSwitchAutoTierStatusPending { + t.Fatalf("Expected status pending, got %q", superseded.Status) + } + if superseded.SupersededAutoTier == nil || *superseded.SupersededAutoTier != rpc.AutoTierEfficiency { + t.Fatalf("Expected superseded efficiency, got %+v", superseded) + } + assertPending(t, session, rpc.AutoTierIntelligence) + + // A nil tier returns the session to provider-default routing. The status is + // unchanged because provider-default was already the committed preference; + // the request's effect is cancelling the staged one. + reset, err := session.SetAutoTier(t.Context(), nil) + if err != nil { + t.Fatalf("SetAutoTier(nil) failed: %v", err) + } + if reset.Status != rpc.ModelSwitchAutoTierStatusUnchanged { + t.Fatalf("Expected status unchanged, got %q", reset.Status) + } + if reset.SupersededAutoTier == nil || *reset.SupersededAutoTier != rpc.AutoTierIntelligence { + t.Fatalf("Expected superseded intelligence, got %+v", reset) + } + assertNoPending(t, session) + }) + + t.Run("should preserve auto tier when set model omits it", func(t *testing.T) { + session := newAutoSession(t) + + if _, err := session.SetAutoTier(t.Context(), autoTier(copilot.AutoTierBalance)); err != nil { + t.Fatalf("SetAutoTier(balance) failed: %v", err) + } + assertPending(t, session, rpc.AutoTierBalance) + + // Leaving AutoTier nil without asking for a reset leaves the staged preference alone. + if err := session.SetModel(t.Context(), "auto", nil); err != nil { + t.Fatalf("SetModel without options failed: %v", err) + } + assertPending(t, session, rpc.AutoTierBalance) + + // Supplying a tier replaces it. + if err := session.SetModel(t.Context(), "auto", &copilot.SetModelOptions{ + AutoTier: autoTier(copilot.AutoTierIntelligence), + }); err != nil { + t.Fatalf("SetModel with AutoTier failed: %v", err) + } + assertPending(t, session, rpc.AutoTierIntelligence) + + // ResetAutoTier clears it. Omission, a value, and a reset are three distinct + // outcomes, which is why a single nillable field cannot express the request. + if err := session.SetModel(t.Context(), "auto", &copilot.SetModelOptions{ + ResetAutoTier: true, + }); err != nil { + t.Fatalf("SetModel with ResetAutoTier failed: %v", err) + } + assertNoPending(t, session) + }) +} diff --git a/go/session.go b/go/session.go index 4c31c01e8c..058fb60a1e 100644 --- a/go/session.go +++ b/go/session.go @@ -4,6 +4,7 @@ package copilot import ( "context" "encoding/json" + "errors" "fmt" "log" "sync" @@ -1841,6 +1842,22 @@ type SetModelOptions struct { // ModelCapabilities overrides individual model capabilities resolved by the runtime. // Only non-nil fields are applied over the runtime-resolved capabilities. ModelCapabilities *rpc.ModelCapabilitiesOverride + // AutoTier stages an Auto routing preference atomically with selecting the + // "auto" model. Leave nil to leave the current preference alone. + // + // The runtime rejects this option when the model is anything other than + // "auto". Use [Session.SetAutoTier] to change the preference without + // changing the selected model. + // + // Experimental: AutoTier is part of an experimental Auto routing surface and + // may change or be removed. + AutoTier *AutoTier + // ResetAutoTier returns to the provider's default Auto routing as part of + // this switch. It is mutually exclusive with AutoTier. + // + // Experimental: ResetAutoTier is part of an experimental Auto routing surface + // and may change or be removed. + ResetAutoTier bool } // SetModel changes the model for this session. @@ -1857,10 +1874,25 @@ type SetModelOptions struct { func (s *Session) SetModel(ctx context.Context, model string, opts *SetModelOptions) error { params := &rpc.ModelSwitchToRequest{ModelID: model} if opts != nil { + if opts.AutoTier != nil && opts.ResetAutoTier { + return errors.New("failed to set model: AutoTier and ResetAutoTier are mutually exclusive") + } params.ReasoningEffort = opts.ReasoningEffort params.ReasoningSummary = opts.ReasoningSummary params.ContextTier = opts.ContextTier params.ModelCapabilities = opts.ModelCapabilities + + // The generated field is a double pointer so the three cases stay + // distinct on the wire: a nil outer pointer omits the field and leaves + // any staged preference alone, while a non-nil outer pointer sends the + // inner value, including an explicit null. + switch { + case opts.AutoTier != nil: + params.AutoTier = &opts.AutoTier + case opts.ResetAutoTier: + var providerDefault *AutoTier + params.AutoTier = &providerDefault + } } _, err := s.RPC.Model.SwitchTo(ctx, params) if err != nil { @@ -1870,6 +1902,41 @@ func (s *Session) SetModel(ctx context.Context, model string, opts *SetModelOpti return nil } +// SetAutoTier changes the Auto routing preference without changing the selected model. +// +// The runtime does not apply the preference immediately. It records the request and +// commits it only when a later user turn using the "auto" model successfully obtains a +// usable model from the provider. A [rpc.ModelSwitchAutoTierStatusPending] status +// therefore confirms that the request was accepted, not that it took effect. +// +// Watch for the outcome through the session.model_change event on success, or the +// ephemeral session.auto_tier_switch_failed event on failure. You can also read the +// current committed and in-flight state at any time with session.RPC.Model.GetCurrent. +// +// Only the most recent request survives: issuing a new request replaces any earlier one +// that has not yet been claimed by a turn. +// +// Pass nil to return to the provider's default Auto routing. +// +// Experimental: SetAutoTier is part of an experimental Auto routing surface and +// may change or be removed. +// +// Example: +// +// tier := copilot.AutoTierIntelligence +// result, err := session.SetAutoTier(context.Background(), &tier) +// if err != nil { +// log.Printf("Failed to set auto tier: %v", err) +// } +func (s *Session) SetAutoTier(ctx context.Context, autoTier *AutoTier) (*rpc.ModelSwitchAutoTierResult, error) { + result, err := s.RPC.Model.SwitchAutoTier(ctx, &rpc.ModelSwitchAutoTierRequest{AutoTier: autoTier}) + if err != nil { + return nil, fmt.Errorf("failed to set auto tier: %w", err) + } + + return result, nil +} + type LogOptions struct { // Level sets the log severity. Valid values are [rpc.SessionLogLevelInfo] (default), // [rpc.SessionLogLevelWarning], and [rpc.SessionLogLevelError]. diff --git a/go/session_event_serialization_test.go b/go/session_event_serialization_test.go index 96bf53bb5e..c64b6ce598 100644 --- a/go/session_event_serialization_test.go +++ b/go/session_event_serialization_test.go @@ -299,3 +299,70 @@ func TestManagedSettingsResolvedProvenanceRoundTrips(t *testing.T) { t.Fatalf("expected absent clientManaged to be omitted, got %v", serialized) } } + +// The failure event is ephemeral: the runtime emits it when an Auto preference +// switch cannot mint a usable model, and never persists or replays it. +func TestSessionAutoTierSwitchFailedEvent(t *testing.T) { + reasons := []AutoTierSwitchFailureReason{ + AutoTierSwitchFailureReasonPolicyRejected, + AutoTierSwitchFailureReasonRequestFailed, + AutoTierSwitchFailureReasonSetupFailed, + AutoTierSwitchFailureReasonUnsupported, + } + for _, reason := range reasons { + t.Run(string(reason), func(t *testing.T) { + wire, err := json.Marshal(map[string]any{ + "id": "00000000-0000-0000-0000-000000000001", + "timestamp": "2026-08-28T00:00:00Z", "parentId": nil, + "type": "session.auto_tier_switch_failed", + "data": map[string]any{ + "effectiveAutoTier": AutoTierBalance, + "requestedAutoTier": AutoTierIntelligence, + "reason": reason, + }, + }) + if err != nil { + t.Fatal(err) + } + var event SessionEvent + if err := json.Unmarshal(wire, &event); err != nil { + t.Fatal(err) + } + data, ok := event.Data.(*SessionAutoTierSwitchFailedData) + if !ok { + t.Fatalf("expected *SessionAutoTierSwitchFailedData, got %T", event.Data) + } + if data.Reason != reason { + t.Fatalf("expected reason %q, got %q", reason, data.Reason) + } + if data.EffectiveAutoTier == nil || *data.EffectiveAutoTier != AutoTierBalance { + t.Fatalf("expected effective tier %q, got %v", AutoTierBalance, data.EffectiveAutoTier) + } + if data.RequestedAutoTier == nil || *data.RequestedAutoTier != AutoTierIntelligence { + t.Fatalf("expected requested tier %q, got %v", AutoTierIntelligence, data.RequestedAutoTier) + } + }) + } +} + +// A null requested tier means the attempt to return to provider-default Auto +// routing is what failed. +func TestSessionAutoTierSwitchFailedEventNullRequestedTier(t *testing.T) { + wire := []byte(`{"id":"00000000-0000-0000-0000-000000000001","timestamp":"2026-08-28T00:00:00Z",` + + `"parentId":null,"type":"session.auto_tier_switch_failed","data":{"effectiveAutoTier":"efficiency",` + + `"requestedAutoTier":null,"reason":"unsupported"}}`) + var event SessionEvent + if err := json.Unmarshal(wire, &event); err != nil { + t.Fatal(err) + } + data, ok := event.Data.(*SessionAutoTierSwitchFailedData) + if !ok { + t.Fatalf("expected *SessionAutoTierSwitchFailedData, got %T", event.Data) + } + if data.RequestedAutoTier != nil { + t.Fatalf("expected nil requested tier, got %v", *data.RequestedAutoTier) + } + if data.EffectiveAutoTier == nil || *data.EffectiveAutoTier != AutoTierEfficiency { + t.Fatalf("expected effective tier %q, got %v", AutoTierEfficiency, data.EffectiveAutoTier) + } +} diff --git a/go/session_test.go b/go/session_test.go index 8d29cfc88f..1547f82375 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -80,6 +80,72 @@ func TestSession_SetModelOmitsContextTierWhenUnset(t *testing.T) { if _, ok := params["contextTier"]; ok { t.Fatalf("expected contextTier to be omitted, got %v", params["contextTier"]) } + if _, ok := params["autoTier"]; ok { + t.Fatalf("expected autoTier to be omitted, got %v", params["autoTier"]) + } +} + +func TestSession_SetModelForwardsAutoTier(t *testing.T) { + tier := AutoTierIntelligence + params := captureSetModelRequestForModel(t, "auto", &SetModelOptions{AutoTier: &tier}) + + if params["modelId"] != "auto" { + t.Fatalf("expected modelId auto, got %v", params["modelId"]) + } + if params["autoTier"] != "intelligence" { + t.Fatalf("expected autoTier intelligence, got %v", params["autoTier"]) + } +} + +func TestSession_SetModelSendsExplicitNullAutoTierWhenCleared(t *testing.T) { + params := captureSetModelRequestForModel(t, "auto", &SetModelOptions{ResetAutoTier: true}) + + // An explicit null must survive to the wire. Omitting it would mean "leave + // the preference alone" rather than "use provider-default routing". + value, ok := params["autoTier"] + if !ok { + t.Fatal("expected autoTier to be present") + } + if value != nil { + t.Fatalf("expected autoTier to be null, got %v", value) + } +} + +func TestSession_SetModelRejectsConflictingAutoTierOptions(t *testing.T) { + tier := AutoTierBalance + session := &Session{SessionID: "session-1"} + + err := session.SetModel(context.Background(), "auto", &SetModelOptions{ + AutoTier: &tier, + ResetAutoTier: true, + }) + if err == nil { + t.Fatal("expected an error when AutoTier and ResetAutoTier are both set") + } +} + +func TestSession_SetAutoTierForwardsTier(t *testing.T) { + tier := AutoTierEfficiency + params := captureSetAutoTierRequest(t, &tier) + + if params["sessionId"] != "session-1" { + t.Fatalf("expected sessionId session-1, got %v", params["sessionId"]) + } + if params["autoTier"] != "efficiency" { + t.Fatalf("expected autoTier efficiency, got %v", params["autoTier"]) + } +} + +func TestSession_SetAutoTierSendsExplicitNull(t *testing.T) { + params := captureSetAutoTierRequest(t, nil) + + value, ok := params["autoTier"] + if !ok { + t.Fatal("expected autoTier to be present") + } + if value != nil { + t.Fatalf("expected autoTier to be null, got %v", value) + } } func TestSession_MCPAuthRequestSendsHostToken(t *testing.T) { @@ -283,6 +349,28 @@ func TestMCPOauthRequiredDataAllowsOptionalMetadata(t *testing.T) { func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any { t.Helper() + return captureModelRequest(t, "session.model.switchTo", func(session *Session) error { + return session.SetModel(context.Background(), "gpt-4.1", opts) + }) +} + +func captureSetModelRequestForModel(t *testing.T, model string, opts *SetModelOptions) map[string]any { + t.Helper() + return captureModelRequest(t, "session.model.switchTo", func(session *Session) error { + return session.SetModel(context.Background(), model, opts) + }) +} + +func captureSetAutoTierRequest(t *testing.T, autoTier *AutoTier) map[string]any { + t.Helper() + return captureModelRequest(t, "session.model.switchAutoTier", func(session *Session) error { + _, err := session.SetAutoTier(context.Background(), autoTier) + return err + }) +} + +func captureModelRequest(t *testing.T, method string, invoke func(*Session) error) map[string]any { + t.Helper() stdinR, stdinW := io.Pipe() stdoutR, stdoutW := io.Pipe() @@ -314,8 +402,8 @@ func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any errCh <- err return } - if request.Method != "session.model.switchTo" { - errCh <- fmt.Errorf("expected session.model.switchTo, got %s", request.Method) + if request.Method != method { + errCh <- fmt.Errorf("expected %s, got %s", method, request.Method) return } @@ -324,7 +412,7 @@ func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any response := map[string]any{ "jsonrpc": "2.0", "id": json.RawMessage(request.ID), - "result": map[string]any{}, + "result": map[string]any{"status": "pending"}, } data, err := json.Marshal(response) if err != nil { @@ -342,8 +430,8 @@ func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any client: client, RPC: rpc.NewSessionRPC(client, "session-1"), } - if err := session.SetModel(context.Background(), "gpt-4.1", opts); err != nil { - t.Fatalf("SetModel failed: %v", err) + if err := invoke(session); err != nil { + t.Fatalf("model request failed: %v", err) } select { @@ -352,7 +440,7 @@ func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any case err := <-errCh: t.Fatal(err) case <-time.After(2 * time.Second): - t.Fatal("timed out waiting for session.model.switchTo request") + t.Fatalf("timed out waiting for %s request", method) } return nil } diff --git a/go/types.go b/go/types.go index 02ca391338..b4a34f2561 100644 --- a/go/types.go +++ b/go/types.go @@ -2315,10 +2315,12 @@ type CapiSessionOptions struct { // AutoTier selects the routing tier for model "auto" with V2 Auto. // Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto. - // When unset, the runtime uses its default on create and preserves the - // persisted or current tier on resume. An explicit tier overrides the - // persisted tier on a cold resume; a conflicting tier on a resident - // session resume is rejected by the runtime. + // When unset, the runtime uses its default on create and restores the last + // committed tier on cold resume. On resident resume, a different tier + // requests a safe switch that takes effect after resume succeeds and never + // disturbs a turn that is already running. + // + // To change the preference on a live session, use [Session.SetAutoTier]. AutoTier AutoTier `json:"autoTier,omitempty"` } diff --git a/java/README.md b/java/README.md index bb71d7ab86..6a95da424e 100644 --- a/java/README.md +++ b/java/README.md @@ -363,12 +363,30 @@ var config = new SessionConfig() The same options work with `ResumeSessionConfig.setCapi(...)` and can be combined with `setEnableWebSocketResponses(false)`. The SDK omits an unset (`null`) tier: the runtime chooses its default on create and preserves the persisted/current -tier on resume. An explicit tier overrides the persisted tier on cold resume; -the runtime rejects a conflicting tier when the session is already resident -in memory. The SDK does not choose a default or manage tier persistence. +tier on resume. An explicit tier overrides the persisted tier on cold resume. On +resident resume, a different tier requests a safe switch applied after the +resume succeeds; it cannot change a turn that is already in flight. The SDK does not choose a default or manage tier persistence. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the lifecycle rules. +### Changing the Auto tier during a session + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```java +var result = session.setAutoTier(AutoTier.INTELLIGENCE).get(); +if (result.status() == ModelSwitchAutoTierStatus.PENDING) { + // Accepted, but not yet in effect. +} + +// Return to the provider's default Auto routing. +session.setAutoTier(null).get(); +``` + +`setModel(SetModelOptions)` accepts the same preference through `SetModelOptions.setAutoTier(...)`, which stages the tier atomically with selecting `auto`. Call `setResetAutoTier(true)` instead to return to provider-default routing; the two options are mutually exclusive. + ## Session Store `enableSessionStore` on `SessionConfig` enables the cross-session store for search and retrieval across sessions. When unset in the default `CopilotClientMode.COPILOT_CLI` mode, the runtime default applies (enabled). In `CopilotClientMode.EMPTY` mode, defaults to disabled. diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index c53fea3f4b..156cb3495a 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.rpc.SessionCommandsHandlePendingCommandParams; import com.github.copilot.generated.rpc.SessionLogParams; @@ -37,6 +38,8 @@ import com.github.copilot.generated.rpc.ModelCapabilitiesOverride; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideLimits; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideSupports; +import com.github.copilot.generated.rpc.SessionModelSwitchAutoTierParams; +import com.github.copilot.generated.rpc.SessionModelSwitchAutoTierResult; import com.github.copilot.generated.rpc.SessionModelSwitchToParams; import com.github.copilot.generated.rpc.SessionPermissionsHandlePendingPermissionRequestParams; import com.github.copilot.generated.rpc.SessionRpc; @@ -2073,21 +2076,7 @@ public CompletableFuture setModel(String model, String reasoningEffort, public CompletableFuture setModel(String model, String reasoningEffort, String reasoningSummary, com.github.copilot.rpc.ModelCapabilitiesOverride modelCapabilities) { ensureNotTerminated(); - ModelCapabilitiesOverride generatedCapabilities = null; - if (modelCapabilities != null) { - ModelCapabilitiesOverrideSupports supports = null; - if (modelCapabilities.getSupports() != null) { - var s = modelCapabilities.getSupports(); - supports = new ModelCapabilitiesOverrideSupports(s.getVision().orElse(null), - s.getReasoningEffort().orElse(null), null); - } - ModelCapabilitiesOverrideLimits limits = null; - if (modelCapabilities.getLimits() != null) { - limits = new ObjectMapper().convertValue(modelCapabilities.getLimits(), - ModelCapabilitiesOverrideLimits.class); - } - generatedCapabilities = new ModelCapabilitiesOverride(supports, limits); - } + ModelCapabilitiesOverride generatedCapabilities = toGeneratedCapabilities(modelCapabilities); var generatedReasoningSummary = reasoningSummary == null ? null : com.github.copilot.generated.rpc.ReasoningSummary.fromValue(reasoningSummary); @@ -2097,6 +2086,131 @@ public CompletableFuture setModel(String model, String reasoningEffort, St .thenApply(r -> null); } + private static ModelCapabilitiesOverride toGeneratedCapabilities( + com.github.copilot.rpc.ModelCapabilitiesOverride modelCapabilities) { + if (modelCapabilities == null) { + return null; + } + ModelCapabilitiesOverrideSupports supports = null; + if (modelCapabilities.getSupports() != null) { + var s = modelCapabilities.getSupports(); + supports = new ModelCapabilitiesOverrideSupports(s.getVision().orElse(null), + s.getReasoningEffort().orElse(null), null); + } + ModelCapabilitiesOverrideLimits limits = null; + if (modelCapabilities.getLimits() != null) { + limits = MAPPER.convertValue(modelCapabilities.getLimits(), ModelCapabilitiesOverrideLimits.class); + } + return new ModelCapabilitiesOverride(supports, limits); + } + + private static com.github.copilot.generated.rpc.AutoTier toGeneratedAutoTier( + com.github.copilot.rpc.AutoTier autoTier) { + return autoTier == null ? null : com.github.copilot.generated.rpc.AutoTier.fromValue(autoTier.getValue()); + } + + /** + * Changes the model for this session using an options object. + *

+ * The new model takes effect for the next message. Conversation history is + * preserved. Use {@link com.github.copilot.rpc.SetModelOptions#setAutoTier} to + * request an Auto routing preference at the same time, which the runtime + * accepts only when the model is {@code "auto"}. + * + *

{@code
+     * session.setModel(new SetModelOptions().setModel("auto").setAutoTier(AutoTier.INTELLIGENCE)).get();
+     * session.setModel(new SetModelOptions().setModel("auto").setResetAutoTier(true)).get();
+     * }
+ * + * @param options + * the switch settings; the model ID is required + * @return a future that completes when the model switch is acknowledged + * @throws IllegalArgumentException + * if {@code options} is {@code null}, if it carries no model ID, or + * if it requests both an explicit Auto tier and a return to + * provider-default Auto routing + * @throws IllegalStateException + * if this session has been terminated + * @since 1.6.0 + */ + public CompletableFuture setModel(com.github.copilot.rpc.SetModelOptions options) { + ensureNotTerminated(); + if (options == null) { + throw new IllegalArgumentException("options must not be null"); + } + if (options.getModel() == null) { + throw new IllegalArgumentException("options must specify a model"); + } + if (options.getAutoTier() != null && options.isResetAutoTier()) { + throw new IllegalArgumentException( + "setModel cannot combine an explicit autoTier with resetAutoTier; choose one"); + } + var generatedReasoningSummary = options.getReasoningSummary() == null + ? null + : com.github.copilot.generated.rpc.ReasoningSummary.fromValue(options.getReasoningSummary()); + var params = new SessionModelSwitchToParams(sessionId, options.getModel(), + toGeneratedAutoTier(options.getAutoTier()), options.getReasoningEffort(), generatedReasoningSummary, + null, toGeneratedCapabilities(options.getModelCapabilities()), null, null, null, null, null, null, null, + null, null); + if (!options.isResetAutoTier()) { + return getRpc().model.switchTo(params).thenApply(r -> null); + } + // The generated params record omits null properties, but returning to + // provider-default Auto routing requires sending an explicit null tier, so + // build the payload directly and reinstate the null. + ObjectNode payload = MAPPER.valueToTree(params); + payload.putNull("autoTier"); + return rpc.invoke("session.model.switchTo", payload, Void.class); + } + + /** + * Changes the Auto routing preference without changing the selected model. + *

+ * The runtime does not apply the preference immediately. It records the request + * and commits it only when a later user turn using the {@code auto} model + * successfully obtains a usable model from the provider. A {@code pending} + * status therefore confirms that the request was accepted, not that it took + * effect. + *

+ * Watch for the outcome through the {@code session.model_change} event on + * success, or the ephemeral {@code session.auto_tier_switch_failed} event on + * failure. You can also read the committed and in-flight state at any time with + * {@code session.getRpc().model.getCurrent()}. + *

+ * Only the most recent request survives: a new request replaces any earlier one + * that no turn has claimed yet. + * + *

{@code
+     * var result = session.setAutoTier(AutoTier.INTELLIGENCE).get();
+     * if (result.status() == ModelSwitchAutoTierStatus.PENDING) {
+     * 	// Takes effect on a later turn that uses the `auto` model.
+     * }
+     * }
+ * + * @param autoTier + * the routing preference to activate, or {@code null} to return to + * the provider's default Auto routing + * @return a future completing with the runtime's immediate acknowledgement and + * Auto preference snapshot + * @throws IllegalStateException + * if this session has been terminated + * @since 1.6.0 + */ + @CopilotExperimental + public CompletableFuture setAutoTier(com.github.copilot.rpc.AutoTier autoTier) { + ensureNotTerminated(); + var params = new SessionModelSwitchAutoTierParams(sessionId, toGeneratedAutoTier(autoTier), null); + if (autoTier != null) { + return getRpc().model.switchAutoTier(params); + } + // The generated params record omits null properties, but the runtime + // distinguishes an explicit null tier (return to provider-default routing) + // from an absent one, so build the payload directly and reinstate the null. + ObjectNode payload = MAPPER.valueToTree(params); + payload.putNull("autoTier"); + return rpc.invoke("session.model.switchAutoTier", payload, SessionModelSwitchAutoTierResult.class); + } + /** * Changes the model for this session. *

diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java index e401762302..1743f572ed 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java @@ -51,8 +51,9 @@ public AutoTier getAutoTier() { *

* When omitted, the runtime chooses its default on create and preserves the * persisted or current tier on resume. An explicit tier overrides the persisted - * tier on cold resume; the runtime rejects a conflicting tier when resuming a - * session already resident in memory. + * tier on cold resume. On resident resume, a different tier requests a safe + * switch that the runtime applies after the resume succeeds; it cannot change a + * turn that is already in flight. * * @param autoTier * the routing tier, or {@code null} to omit it from the request diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SetModelOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/SetModelOptions.java new file mode 100644 index 0000000000..9ebfbf199b --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SetModelOptions.java @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.github.copilot.CopilotExperimental; + +/** + * Optional settings for a model switch. + *

+ * All setter methods return {@code this} for method chaining. {@code model} is + * required. Every other option is optional; an unset option leaves the + * corresponding session state unchanged. + * + *

{@code
+ * session.setModel(new SetModelOptions().setModel("auto").setAutoTier(AutoTier.INTELLIGENCE)).get();
+ * }
+ * + * @since 1.6.0 + */ +public class SetModelOptions { + + private String model; + + private String reasoningEffort; + + private String reasoningSummary; + + private ModelCapabilitiesOverride modelCapabilities; + + private AutoTier autoTier; + + private boolean resetAutoTier; + + /** + * Gets the target model ID. + * + * @return the model ID, or {@code null} when none has been set + */ + public String getModel() { + return model; + } + + /** + * Sets the model to switch to. This option is required. + * + * @param model + * the model ID (e.g., {@code "gpt-5.4"} or {@code "auto"}) + * @return this options object for method chaining + */ + public SetModelOptions setModel(String model) { + this.model = model; + return this; + } + + /** + * Gets the reasoning effort level. + * + * @return the reasoning effort level, or {@code null} to use the default + */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets the reasoning effort level. + * + * @param reasoningEffort + * reasoning effort level (e.g., {@code "low"}, {@code "medium"}, + * {@code "high"}, {@code "xhigh"}, {@code "max"}); {@code null} to + * use the default + * @return this options object for method chaining + */ + public SetModelOptions setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } + + /** + * Gets the reasoning summary mode. + * + * @return the reasoning summary mode, or {@code null} to use the default + */ + public String getReasoningSummary() { + return reasoningSummary; + } + + /** + * Sets the reasoning summary mode. + * + * @param reasoningSummary + * reasoning summary mode ({@code "none"}, {@code "concise"}, or + * {@code "detailed"}); {@code null} to use the default + * @return this options object for method chaining + */ + public SetModelOptions setReasoningSummary(String reasoningSummary) { + this.reasoningSummary = reasoningSummary; + return this; + } + + /** + * Gets the model capability overrides. + * + * @return the capability overrides, or {@code null} to use runtime defaults + */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets per-property overrides for model capabilities. + * + * @param modelCapabilities + * the capability overrides; {@code null} to use runtime defaults + * @return this options object for method chaining + */ + public SetModelOptions setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + return this; + } + + /** + * Gets the requested Auto routing preference. + * + * @return the requested tier, or {@code null} when no tier was requested + */ + @CopilotExperimental + public AutoTier getAutoTier() { + return autoTier; + } + + /** + * Requests an Auto routing preference alongside the model switch. + *

+ * The runtime records the request and commits it only when a later user turn + * using the {@code auto} model successfully obtains a usable model from the + * provider. Use {@link #setResetAutoTier(boolean)} to return to the provider's + * default Auto routing instead. + * + * @param autoTier + * the routing preference to request; {@code null} to leave the + * current preference unchanged + * @return this options object for method chaining + */ + @CopilotExperimental + public SetModelOptions setAutoTier(AutoTier autoTier) { + this.autoTier = autoTier; + return this; + } + + /** + * Gets whether the request returns to provider-default Auto routing. + * + * @return {@code true} when the request clears the Auto routing preference + */ + @CopilotExperimental + public boolean isResetAutoTier() { + return resetAutoTier; + } + + /** + * Requests a return to the provider's default Auto routing. + *

+ * This differs from leaving {@link #setAutoTier(AutoTier)} unset, which keeps + * the current preference. It cannot be combined with an explicit tier. + * + * @param resetAutoTier + * {@code true} to return to provider-default Auto routing + * @return this options object for method chaining + */ + @CopilotExperimental + public SetModelOptions setResetAutoTier(boolean resetAutoTier) { + this.resetAutoTier = resetAutoTier; + return this; + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/AutoTierIT.java b/java/sdk/src/test/java/com/github/copilot/AutoTierIT.java new file mode 100644 index 0000000000..d486ce1c2c --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/AutoTierIT.java @@ -0,0 +1,120 @@ +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.generated.rpc.ModelSwitchAutoTierStatus; +import com.github.copilot.rpc.AutoTier; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.SetModelOptions; + +/** + * End-to-end coverage for Auto tier switching, mirroring + * {@code nodejs/test/e2e/auto_tier.e2e.test.ts}. + *

+ * The runtime stages an Auto routing preference rather than applying it + * immediately: a request stays unclaimed until a later turn using the + * {@code auto} model mints a usable model and token pair. These tests read the + * staged state back through {@code model.getCurrent()}, so they assert what the + * runtime actually recorded rather than what the SDK serialized. + */ +class AutoTierIT { + + private static final String MODEL_ID = "auto"; + + private static E2ETestContext ctx; + + @BeforeAll + static void setUp() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void tearDown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + private static com.github.copilot.generated.rpc.AutoTier pendingAutoTier(CopilotSession session) throws Exception { + return session.getRpc().model.getCurrent().get(30, TimeUnit.SECONDS).pendingAutoTier(); + } + + private static CopilotSession createAutoSession(CopilotClient client) throws Exception { + return client + .createSession( + new SessionConfig().setModel(MODEL_ID).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + } + + @Test + void shouldStageAndResetAutoTierPreference() throws Exception { + ctx.configureForTest("auto_tier", "should_stage_and_reset_auto_tier_preference"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = createAutoSession(client); + try { + assertNull(pendingAutoTier(session)); + + var staged = session.setAutoTier(AutoTier.EFFICIENCY).get(30, TimeUnit.SECONDS); + assertEquals(ModelSwitchAutoTierStatus.PENDING, staged.status()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.EFFICIENCY, staged.pendingAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.EFFICIENCY, pendingAutoTier(session)); + + // A second request replaces the first and reports the one it displaced. + var superseded = session.setAutoTier(AutoTier.INTELLIGENCE).get(30, TimeUnit.SECONDS); + assertEquals(ModelSwitchAutoTierStatus.PENDING, superseded.status()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, superseded.pendingAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.EFFICIENCY, superseded.supersededAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, pendingAutoTier(session)); + + // A null tier returns the session to provider-default routing. The status + // is `unchanged` because provider-default was already the committed + // preference; the request's effect is cancelling the staged one. + var reset = session.setAutoTier(null).get(30, TimeUnit.SECONDS); + assertEquals(ModelSwitchAutoTierStatus.UNCHANGED, reset.status()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, reset.supersededAutoTier()); + assertNull(pendingAutoTier(session)); + } finally { + session.close(); + } + } + } + + @Test + void shouldPreserveAutoTierWhenSetModelOmitsIt() throws Exception { + ctx.configureForTest("auto_tier", "should_preserve_auto_tier_when_set_model_omits_it"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = createAutoSession(client); + try { + session.setAutoTier(AutoTier.BALANCE).get(30, TimeUnit.SECONDS); + assertEquals(com.github.copilot.generated.rpc.AutoTier.BALANCE, pendingAutoTier(session)); + + // Omitting the preference leaves the staged one alone. + session.setModel(new SetModelOptions().setModel(MODEL_ID)).get(30, TimeUnit.SECONDS); + assertEquals(com.github.copilot.generated.rpc.AutoTier.BALANCE, pendingAutoTier(session)); + + // Supplying a tier replaces it. + session.setModel(new SetModelOptions().setModel(MODEL_ID).setAutoTier(AutoTier.INTELLIGENCE)).get(30, + TimeUnit.SECONDS); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, pendingAutoTier(session)); + + // Requesting a reset clears it. Omission, an explicit tier, and a reset + // are three distinct outcomes. + session.setModel(new SetModelOptions().setModel(MODEL_ID).setResetAutoTier(true)).get(30, + TimeUnit.SECONDS); + assertNull(pendingAutoTier(session)); + } finally { + session.close(); + } + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java index 5213cdbb8d..25e356a8d1 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java @@ -16,6 +16,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.copilot.generated.AutoTier; +import com.github.copilot.generated.AutoTierSwitchFailureReason; +import com.github.copilot.generated.SessionAutoTierSwitchFailedEvent; import com.github.copilot.generated.SessionEvent; import com.github.copilot.generated.SessionResumeEvent; import com.github.copilot.generated.SessionStartEvent; @@ -64,4 +66,39 @@ private static AutoTier autoTier(SessionEvent event, String type) { } return assertInstanceOf(SessionResumeEvent.class, event).getData().autoTier(); } + + @ParameterizedTest + @CsvSource({"policy_rejected,POLICY_REJECTED", "request_failed,REQUEST_FAILED", "setup_failed,SETUP_FAILED", + "unsupported,UNSUPPORTED"}) + void autoTierSwitchFailedEventDecodesEveryReason(String value, AutoTierSwitchFailureReason reason) + throws Exception { + String json = """ + {"type":"session.auto_tier_switch_failed","data":{"effectiveAutoTier":"balance", + "requestedAutoTier":"intelligence","reason":"%s"}} + """.formatted(value); + + var event = MAPPER.readValue(json, SessionEvent.class); + + var data = assertInstanceOf(SessionAutoTierSwitchFailedEvent.class, event).getData(); + assertEquals(AutoTier.BALANCE, data.effectiveAutoTier()); + assertEquals(AutoTier.INTELLIGENCE, data.requestedAutoTier()); + assertEquals(reason, data.reason()); + } + + @org.junit.jupiter.api.Test + void autoTierSwitchFailedEventAllowsNullRequestedTier() throws Exception { + // A null requested tier means the attempt to return to provider-default + // Auto routing is what failed. + String json = """ + {"type":"session.auto_tier_switch_failed","data":{"effectiveAutoTier":"efficiency", + "requestedAutoTier":null,"reason":"unsupported"}} + """; + + var event = MAPPER.readValue(json, SessionEvent.class); + + var data = assertInstanceOf(SessionAutoTierSwitchFailedEvent.class, event).getData(); + assertEquals(AutoTier.EFFICIENCY, data.effectiveAutoTier()); + assertNull(data.requestedAutoTier()); + assertEquals(AutoTierSwitchFailureReason.UNSUPPORTED, data.reason()); + } } diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java new file mode 100644 index 0000000000..adeeca2c6b --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java @@ -0,0 +1,228 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.rpc.ModelSwitchAutoTierStatus; +import com.github.copilot.generated.rpc.SessionModelSwitchAutoTierResult; +import com.github.copilot.rpc.AutoTier; +import com.github.copilot.rpc.SetModelOptions; +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; +import org.junit.jupiter.api.Test; + +/** + * Verifies the wire payloads produced by Auto routing preference switches. + *

+ * The runtime treats an explicit {@code null} {@code autoTier} (return to + * provider-default routing) differently from an absent one (leave the + * preference unchanged), so these tests assert on the raw JSON rather than on + * the generated params records, which drop null properties. + */ +@AllowCopilotExperimental +class SessionAutoTierSwitchTest { + + @Test + void setModel_omits_autoTier_when_no_preference_is_requested() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-1", sockets.client()); + var stub = sockets.stubServer(); + + session.setModel(new SetModelOptions().setModel("auto")); + + var params = stub.readOneMessage().get("params"); + assertEquals("auto", params.get("modelId").asText()); + assertFalse(params.has("autoTier"), "an unset preference must not appear on the wire"); + } + } + + @Test + void setModel_sends_requested_autoTier() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-2", sockets.client()); + var stub = sockets.stubServer(); + + session.setModel(new SetModelOptions().setModel("auto").setAutoTier(AutoTier.INTELLIGENCE) + .setReasoningEffort("high")); + + var sent = stub.readOneMessage(); + assertEquals("session.model.switchTo", sent.get("method").asText()); + var params = sent.get("params"); + assertEquals("intelligence", params.get("autoTier").asText()); + assertEquals("high", params.get("reasoningEffort").asText()); + assertEquals("sess-2", params.get("sessionId").asText()); + } + } + + @Test + void setModel_sends_explicit_null_autoTier_when_clearing() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-3", sockets.client()); + var stub = sockets.stubServer(); + + session.setModel(new SetModelOptions().setModel("auto").setResetAutoTier(true)); + + var sent = stub.readOneMessage(); + assertEquals("session.model.switchTo", sent.get("method").asText()); + var params = sent.get("params"); + assertTrue(params.has("autoTier"), "clearing must send the property"); + assertTrue(params.get("autoTier").isNull(), "clearing must send an explicit null"); + assertEquals("sess-3", params.get("sessionId").asText()); + } + } + + @Test + void setModel_rejects_a_tier_combined_with_clearing() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-4", sockets.client()); + + var options = new SetModelOptions().setModel("auto").setAutoTier(AutoTier.BALANCE).setResetAutoTier(true); + + assertThrows(IllegalArgumentException.class, () -> session.setModel(options)); + } + } + + @Test + void setModel_requires_a_model() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-5", sockets.client()); + + assertThrows(IllegalArgumentException.class, () -> session.setModel(new SetModelOptions())); + assertThrows(IllegalArgumentException.class, () -> session.setModel((SetModelOptions) null)); + } + } + + @Test + void setAutoTier_sends_the_requested_tier() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-6", sockets.client()); + var stub = sockets.stubServer(); + + session.setAutoTier(AutoTier.EFFICIENCY); + + var sent = stub.readOneMessage(); + assertEquals("session.model.switchAutoTier", sent.get("method").asText()); + var params = sent.get("params"); + assertEquals("efficiency", params.get("autoTier").asText()); + assertEquals("sess-6", params.get("sessionId").asText()); + } + } + + @Test + void setAutoTier_sends_explicit_null_for_provider_default_routing() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-7", sockets.client()); + var stub = sockets.stubServer(); + + session.setAutoTier(null); + + var sent = stub.readOneMessage(); + assertEquals("session.model.switchAutoTier", sent.get("method").asText()); + var params = sent.get("params"); + assertTrue(params.has("autoTier"), "returning to provider-default routing must send the property"); + assertTrue(params.get("autoTier").isNull(), "returning to provider-default routing must send null"); + assertEquals("sess-7", params.get("sessionId").asText()); + } + } + + @Test + void switchAutoTier_result_deserializes_every_field() throws Exception { + var json = """ + { + "status": "pending", + "effectiveAutoTier": "balance", + "pendingAutoTier": "intelligence", + "activatingAutoTier": null, + "supersededAutoTier": "efficiency" + } + """; + + var result = new ObjectMapper().readValue(json, SessionModelSwitchAutoTierResult.class); + + assertEquals(ModelSwitchAutoTierStatus.PENDING, result.status()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.BALANCE, result.effectiveAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, result.pendingAutoTier()); + assertNull(result.activatingAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.EFFICIENCY, result.supersededAutoTier()); + } + + /** + * Loopback socket pair; the client side backs a real {@link JsonRpcClient} and + * the server side exposes the raw outbound messages. + */ + private static final class SocketPair implements AutoCloseable { + + private final Socket clientSocket; + private final Socket serverSocket; + private final JsonRpcClient rpcClient; + + SocketPair() throws Exception { + try (var ss = new ServerSocket(0)) { + clientSocket = new Socket("localhost", ss.getLocalPort()); + serverSocket = ss.accept(); + } + serverSocket.setSoTimeout(3000); + rpcClient = JsonRpcClient.fromSocket(clientSocket); + } + + JsonRpcClient client() { + return rpcClient; + } + + StubServer stubServer() { + return new StubServer(serverSocket); + } + + @Override + public void close() throws Exception { + rpcClient.close(); + clientSocket.close(); + serverSocket.close(); + } + } + + /** Reads Content-Length framed JSON-RPC messages from the server socket. */ + private static final class StubServer { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private final InputStream in; + + StubServer(Socket socket) { + try { + this.in = socket.getInputStream(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + JsonNode readOneMessage() throws Exception { + var header = new StringBuilder(); + int b; + while ((b = in.read()) != -1) { + if (b == '\n' && header.toString().endsWith("\r")) { + break; + } + header.append((char) b); + } + in.read(); + in.read(); + + String hdr = header.toString().trim(); + int colon = hdr.indexOf(':'); + int len = Integer.parseInt(hdr.substring(colon + 1).trim()); + byte[] body = in.readNBytes(len); + return MAPPER.readTree(body); + } + } +} diff --git a/nodejs/README.md b/nodejs/README.md index 3a2a536e65..e3d76ba6e4 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -319,6 +319,32 @@ const unsubscribe = session.on((event) => { unsubscribe(); ``` +##### `setModel(model: string, options?): Promise` + +Change the model for this session. The new model takes effect for the next message; conversation history is preserved. + +**Options:** + +- `reasoningEffort?: string` - Reasoning effort level +- `autoTier?: AutoTier | null` - Auto routing preference to stage together with selecting `auto`. Pass `null` to return to the provider's default Auto routing; omit it to leave the current preference unchanged. + +##### `setAutoTier(autoTier: AutoTier | null): Promise` + +Change the Auto routing preference without changing the selected model. Pass `null` to return to the provider's default Auto routing. + +The runtime does not apply the preference immediately. It records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure, and read the authoritative state at any time with `session.rpc.model.getCurrent()`. + +```typescript +const result = await session.setAutoTier("intelligence"); +if (result.status === "pending") { + // Accepted, but not yet in effect. +} +``` + +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. + ##### `abort(): Promise` Abort the currently processing message in this session. diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index bf6f2195f9..2007679d61 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -123,6 +123,9 @@ export type { ModelBillingTokenPricesLongContext, AutoTier, CapiSessionOptions, + CurrentModel, + ModelSwitchAutoTierResult, + ModelSwitchAutoTierStatus, ModelCapabilities, ModelCapabilitiesOverride, ModelInfo, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index eb4c6b7561..0dfff88bc7 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -18,6 +18,7 @@ import type { McpOauthPendingRequestResponse, FactoryLogLine, FactoryRunResult as WireFactoryRunResult, + ModelSwitchAutoTierResult, } from "./generated/rpc.js"; import { type Canvas, CanvasError } from "./canvas.js"; import type { OpenCanvasInstance } from "./generated/rpc.js"; @@ -46,6 +47,7 @@ import type { ContextTier, ReasoningEffort, ReasoningSummary, + AutoTier, ModelCapabilitiesOverride, SectionTransformFn, SessionCapabilities, @@ -2065,6 +2067,9 @@ export class CopilotSession { * ```typescript * await session.setModel("gpt-5.4"); * await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" }); + * + * // Select the Auto model and its routing preference in one call. + * await session.setModel("auto", { autoTier: "intelligence" }); * ``` */ async setModel( @@ -2074,11 +2079,58 @@ export class CopilotSession { reasoningSummary?: ReasoningSummary; contextTier?: ContextTier; modelCapabilities?: ModelCapabilitiesOverride; + /** + * Routing preference to apply when `model` is `auto`. + * + * Pass `null` to return to the provider's default Auto routing. The + * runtime rejects this option when `model` is anything other than + * `auto`; use {@link setAutoTier} to change the preference without + * changing the selected model. + * + * @experimental Part of an experimental Auto routing surface and may + * change or be removed in a future release. + */ + autoTier?: AutoTier | null; } ): Promise { await this.rpc.model.switchTo({ modelId: model, ...options }); } + /** + * Change the Auto routing preference without changing the selected model. + * + * The runtime does not apply the preference immediately. It records the + * request and commits it only when a later user turn using the `auto` model + * successfully obtains a usable model from the provider. A `pending` status + * therefore confirms that the request was accepted, not that it took effect. + * + * Watch for the outcome through the `session.model_change` event on success, + * or the ephemeral `session.auto_tier_switch_failed` event on failure. You + * can also read the current committed and in-flight state at any time with + * `session.rpc.model.getCurrent()`. + * + * Only the most recent request survives: issuing a new request replaces any + * earlier one that has not yet been claimed by a turn. + * + * @param autoTier - Routing preference to activate, or `null` to return to + * the provider's default Auto routing + * @returns The runtime's immediate acknowledgement and Auto preference snapshot + * + * @experimental Part of an experimental Auto routing surface and may change + * or be removed in a future release. + * + * @example + * ```typescript + * const result = await session.setAutoTier("intelligence"); + * if (result.status === "pending") { + * // Takes effect on a later turn that uses the `auto` model. + * } + * ``` + */ + async setAutoTier(autoTier: AutoTier | null): Promise { + return await this.rpc.model.switchAutoTier({ autoTier }); + } + /** * Log a message to the session timeline. * The message appears in the session event stream and is visible to SDK consumers diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 128ced3d68..82c6fe0e53 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -74,6 +74,11 @@ export type SessionEvent = | Exclude | PermissionRequestedEvent; export type { AutoTier, ReasoningSummary } from "./generated/session-events.js"; +export type { + CurrentModel, + ModelSwitchAutoTierResult, + ModelSwitchAutoTierStatus, +} from "./generated/rpc.js"; export type { SessionFsProvider } from "./sessionFsProvider.js"; export { createSessionFsAdapter } from "./sessionFsProvider.js"; export type { SessionFsFileInfo } from "./sessionFsProvider.js"; @@ -2176,9 +2181,13 @@ export interface CapiSessionOptions { * Requires a runtime with Auto tier support and V2 Auto routing. * * When omitted on create, the runtime uses its default routing behavior. - * The runtime persists this preference across cold resume; an explicit tier - * on cold resume overrides the persisted value. For an already-resident - * session, omission preserves the current tier and a different tier is rejected. + * The runtime persists this preference across cold resume; when omitted on + * cold resume, it restores the last committed preference. On resident + * resume, a different tier requests a safe switch that takes effect after + * resume succeeds, and never disturbs a turn that is already running. + * + * To change the preference on a live session, call + * {@link CopilotSession.setAutoTier} instead. */ autoTier?: AutoTier; diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 098d9eac54..ba96292ba0 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -2604,6 +2604,117 @@ describe("CopilotClient", () => { spy.mockRestore(); }); + it("sends the auto tier with session.model.switchTo when selecting the auto model", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, _params: any) => { + if (method === "session.model.switchTo") return {}; + throw new Error(`Unexpected method: ${method}`); + }); + + await session.setModel("auto", { autoTier: "intelligence" }); + + expect(spy).toHaveBeenCalledWith("session.model.switchTo", { + sessionId: session.sessionId, + modelId: "auto", + autoTier: "intelligence", + }); + + spy.mockRestore(); + }); + + it("sends a null auto tier with session.model.switchTo to restore default routing", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, _params: any) => { + if (method === "session.model.switchTo") return {}; + throw new Error(`Unexpected method: ${method}`); + }); + + await session.setModel("auto", { autoTier: null }); + + expect(spy).toHaveBeenCalledWith("session.model.switchTo", { + sessionId: session.sessionId, + modelId: "auto", + autoTier: null, + }); + + spy.mockRestore(); + }); + + it("sends session.model.switchAutoTier RPC and returns the runtime snapshot", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, _params: any) => { + if (method === "session.model.switchAutoTier") { + return { + status: "pending", + effectiveAutoTier: "balance", + pendingAutoTier: "intelligence", + activatingAutoTier: null, + supersededAutoTier: null, + }; + } + throw new Error(`Unexpected method: ${method}`); + }); + + const result = await session.setAutoTier("intelligence"); + + expect(spy).toHaveBeenCalledWith("session.model.switchAutoTier", { + sessionId: session.sessionId, + autoTier: "intelligence", + }); + expect(result.status).toBe("pending"); + expect(result.effectiveAutoTier).toBe("balance"); + expect(result.pendingAutoTier).toBe("intelligence"); + expect(result.activatingAutoTier).toBeNull(); + + spy.mockRestore(); + }); + + it("sends a null auto tier with session.model.switchAutoTier to restore default routing", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, _params: any) => { + if (method === "session.model.switchAutoTier") return { status: "unchanged" }; + throw new Error(`Unexpected method: ${method}`); + }); + + const result = await session.setAutoTier(null); + + expect(spy).toHaveBeenCalledWith("session.model.switchAutoTier", { + sessionId: session.sessionId, + autoTier: null, + }); + expect(result.status).toBe("unchanged"); + + spy.mockRestore(); + }); + describe("URL parsing", () => { it("should parse port-only URL format", () => { const client = new CopilotClient({ diff --git a/nodejs/test/e2e/auto_tier.e2e.test.ts b/nodejs/test/e2e/auto_tier.e2e.test.ts new file mode 100644 index 0000000000..0cb2a1a266 --- /dev/null +++ b/nodejs/test/e2e/auto_tier.e2e.test.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +/** + * The runtime stages an Auto routing preference instead of applying it immediately: a + * request is "unclaimed" until a later turn using the `auto` model mints a usable model + * and token pair. These tests observe that staged state through `model.getCurrent`, so + * they assert what the runtime actually recorded rather than what the SDK serialized. + */ +describe("Auto tier switching", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + it("should stage and reset auto tier preference", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "auto", + }); + + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBeUndefined(); + + const staged = await session.setAutoTier("efficiency"); + expect(staged.status).toBe("pending"); + expect(staged.pendingAutoTier).toBe("efficiency"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("efficiency"); + + // A second request replaces the first and reports the one it displaced. + const superseded = await session.setAutoTier("intelligence"); + expect(superseded.status).toBe("pending"); + expect(superseded.pendingAutoTier).toBe("intelligence"); + expect(superseded.supersededAutoTier).toBe("efficiency"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("intelligence"); + + // Passing null returns the session to provider-default routing. The status is + // `unchanged` because provider-default was already the committed preference; + // the request's effect is cancelling the staged one. + const reset = await session.setAutoTier(null); + expect(reset.status).toBe("unchanged"); + expect(reset.supersededAutoTier).toBe("intelligence"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBeUndefined(); + + await session.disconnect(); + }); + + it("should preserve auto tier when set model omits it", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "auto", + }); + + await session.setAutoTier("balance"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("balance"); + + // Omitting the option leaves the staged preference alone. + await session.setModel("auto"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("balance"); + + // Supplying a tier replaces it. + await session.setModel("auto", { autoTier: "intelligence" }); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("intelligence"); + + // Supplying null clears it. Omission, a value, and null are three distinct + // outcomes, which is why the option cannot collapse to a plain optional field. + await session.setModel("auto", { autoTier: null }); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBeUndefined(); + + await session.disconnect(); + }); +}); diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts index 93edebfc80..d20f3caaf6 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -22,6 +22,9 @@ import type { // The aggregate union; must still resolve via the package root. SessionEvent, AutoTier, + AutoTierSwitchFailedData, + AutoTierSwitchFailedEvent, + AutoTierSwitchFailureReason, CapiSessionOptions, PermissionRequest, PermissionRequestedData, @@ -156,6 +159,46 @@ describe("Session event type exports (#1156)", () => { } }); + it.each([ + "policy_rejected", + "request_failed", + "setup_failed", + "unsupported", + ] satisfies AutoTierSwitchFailureReason[])( + "exposes the Auto tier switch failure event with reason %s", + (reason) => { + const data: AutoTierSwitchFailedData = { + reason, + requestedAutoTier: "intelligence", + effectiveAutoTier: "balance", + }; + const event: AutoTierSwitchFailedEvent = { + type: "session.auto_tier_switch_failed", + id: "event-1", + parentId: null, + timestamp: "2026-09-02T00:00:00Z", + ephemeral: true, + data, + }; + + // The failure event must be reachable through the aggregate union so + // consumers can narrow on it in a single event handler. + const asSessionEvent: SessionEvent = event; + expect(asSessionEvent.type).toBe("session.auto_tier_switch_failed"); + expect(data.reason).toBe(reason); + expect(data.requestedAutoTier).toBe("intelligence"); + } + ); + + it("allows a null requested Auto tier when returning to default routing fails", () => { + const data: AutoTierSwitchFailedData = { + reason: "unsupported", + requestedAutoTier: null, + }; + expect(data.requestedAutoTier).toBeNull(); + expect(data.effectiveAutoTier).toBeUndefined(); + }); + it("exposes the headline ToolExecutionStartData type with a usable shape", () => { // This is the specific type called out in issue #1156. The annotation // is the compile-time API-surface check; these assertions only validate diff --git a/python/README.md b/python/README.md index 359026df41..6a7cb8a206 100644 --- a/python/README.md +++ b/python/README.md @@ -460,6 +460,25 @@ async def lookup_issue(params: LookupParams) -> str: # your logic ``` +## Auto routing tiers + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```python +result = await session.set_auto_tier("intelligence") +if result.status == ModelSwitchAutoTierStatus.PENDING: + ... # Accepted, but not yet in effect. + +# Return to the provider's default Auto routing. +await session.set_auto_tier(None) +``` + +`set_model()` accepts the same preference through its `auto_tier` argument, which stages the tier atomically with selecting `auto`. Pass `None` to return to provider-default routing, or omit the argument to leave the current preference unchanged. + +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. + ## Image Support The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 5d9e3f9500..8e14887a4f 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -90,6 +90,7 @@ LlmInferenceHeaders, ) from .generated.rpc import ( + CurrentModel, CurrentToolMetadata, GitHubTelemetryClientInfo, GitHubTelemetryEvent, @@ -99,6 +100,8 @@ GitHubTokenAcquireResultKind, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, + ModelSwitchAutoTierResult, + ModelSwitchAutoTierStatus, PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, @@ -106,7 +109,9 @@ PermissionResponseCapability, ) from .generated.session_events import ( + AutoTierSwitchFailureReason, PermissionRequest, + SessionAutoTierSwitchFailedData, SessionEvent, SessionEventType, ) @@ -234,6 +239,11 @@ "AutoModeSwitchResponse", "AskUserVariant", "AutoTier", + "SessionAutoTierSwitchFailedData", + "AutoTierSwitchFailureReason", + "CurrentModel", + "ModelSwitchAutoTierResult", + "ModelSwitchAutoTierStatus", "BUILTIN_TOOLS_ISOLATED", "CanvasAction", "CanvasDeclaration", diff --git a/python/copilot/client.py b/python/copilot/client.py index ab3410a56c..2df2db80a5 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -92,6 +92,7 @@ ) from .session import ( AutoModeSwitchHandler, + AutoTier, BearerTokenProvider, CommandDefinition, ContextTier, @@ -264,10 +265,6 @@ def _exp_assignment_response_to_dict( return wire -AutoTier = Literal["efficiency", "balance", "intelligence"] -"""Routing preference used when the session model is ``auto``.""" - - class CapiSessionOptions(TypedDict, total=False): """Provider-scoped Copilot API (CAPI) session options.""" @@ -276,9 +273,13 @@ class CapiSessionOptions(TypedDict, total=False): Requires a runtime with Auto tier support and V2 Auto routing. When omitted on create, the runtime uses its default routing behavior. The runtime persists - this preference across cold resume; an explicit tier on cold resume overrides - the persisted value. For an already-resident session, omission preserves the - current tier and a different tier is rejected. + this preference across cold resume; when omitted on cold resume, it restores + the last committed preference. On resident resume, a different tier requests a + safe switch that takes effect after resume succeeds and never disturbs a turn + that is already running. + + To change the preference on a live session, call + :meth:`CopilotSession.set_auto_tier` instead. """ enable_web_socket_responses: bool diff --git a/python/copilot/session.py b/python/copilot/session.py index 3c6d3d54a4..d92fc7f34f 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -19,6 +19,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import UTC, datetime +from enum import Enum from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, NotRequired, Required, TypedDict, cast @@ -26,6 +27,9 @@ from ._jsonrpc import JsonRpcError, ProcessExitedError from ._telemetry import get_trace_context, trace_context from .canvas import CanvasError, CanvasHandler, OpenCanvasInstance +from .generated.rpc import ( + AutoTier as _RpcAutoTier, +) from .generated.rpc import ( BuiltinToolInputSchemaType, CanvasProviderCloseRequest, @@ -39,6 +43,7 @@ LogRequest, MCPOauthHandlePendingRequest, MCPOauthPendingRequestResponse, + ModelSwitchAutoTierResult, ModelSwitchToRequest, PermissionDecision, PermissionDecisionApproveOnce, @@ -174,9 +179,37 @@ def _capabilities_to_dict(caps: ModelCapabilitiesOverride) -> dict: ReasoningEffort = Literal["low", "medium", "high", "xhigh", "max"] ReasoningSummary = Literal["none", "concise", "detailed"] ContextTier = Literal["default", "long_context"] +AutoTier = Literal["efficiency", "balance", "intelligence"] SessionFsConventions = Literal["posix", "windows"] +class _Unset: + """Sentinel distinguishing an omitted argument from an explicit ``None``. + + Auto routing treats ``None`` as a meaningful value: it means "return to the + provider's default routing". Omitting the argument instead means "leave the + current preference alone", so the two cases cannot share a default. + """ + + def __repr__(self) -> str: + return "UNSET" + + +_UNSET = _Unset() + + +def _auto_tier_to_wire(auto_tier: AutoTier | _RpcAutoTier | None) -> str | None: + """Normalize an Auto tier to its wire value. + + Callers may pass either the ``AutoTier`` string literal or the generated + ``AutoTier`` enum, which is the type the SDK hands back on results and + events. The JSON-RPC encoder only understands plain strings. + """ + if isinstance(auto_tier, Enum): + return str(auto_tier.value) + return auto_tier + + class SessionFsCapabilities(TypedDict, total=False): sqlite: bool @@ -3050,6 +3083,7 @@ async def set_model( reasoning_summary: ReasoningSummary | None = None, context_tier: ContextTier | None = None, model_capabilities: ModelCapabilitiesOverride | None = None, + auto_tier: AutoTier | _RpcAutoTier | None | _Unset = _UNSET, ) -> None: """ Change the model for this session. @@ -3067,6 +3101,14 @@ async def set_model( context_tier: Optional context window tier for supported models. Omit to use normal model behavior with no explicit tier. model_capabilities: Override individual model capabilities resolved by the runtime. + auto_tier: **Experimental.** Part of an experimental Auto routing + surface and may change or be removed in a future release. + Routing preference to apply when ``model`` is ``"auto"``. + Pass ``None`` to return to the provider's default Auto routing. + Omit the argument to leave the current preference alone. The + runtime rejects this option when ``model`` is anything other than + ``"auto"``; use :meth:`set_auto_tier` to change the preference + without changing the selected model. Raises: Exception: If the session has been destroyed or the connection fails. @@ -3074,23 +3116,79 @@ async def set_model( Example: >>> await session.set_model("gpt-5.4") >>> await session.set_model("claude-sonnet-4.6", reasoning_effort="high") + >>> await session.set_model("auto", auto_tier="intelligence") """ rpc_caps = None if model_capabilities is not None: rpc_caps = _RpcModelCapabilitiesOverride.from_dict( _capabilities_to_dict(model_capabilities) ) - await self.rpc.model.switch_to( - ModelSwitchToRequest( - model_id=model, - reasoning_effort=reasoning_effort, - reasoning_summary=( - _RpcReasoningSummary(reasoning_summary) - if reasoning_summary is not None - else None - ), - context_tier=(_RpcContextTier(context_tier) if context_tier is not None else None), - model_capabilities=rpc_caps, + request = ModelSwitchToRequest( + model_id=model, + reasoning_effort=reasoning_effort, + reasoning_summary=( + _RpcReasoningSummary(reasoning_summary) if reasoning_summary is not None else None + ), + context_tier=(_RpcContextTier(context_tier) if context_tier is not None else None), + model_capabilities=rpc_caps, + ) + if isinstance(auto_tier, _Unset): + await self.rpc.model.switch_to(request) + return + + # The generated wrapper drops null fields, which would silently turn a + # request for default Auto routing into "leave the preference alone", so + # send the payload directly to preserve an explicit null. + params = {k: v for k, v in request.to_dict().items() if v is not None} + params["autoTier"] = _auto_tier_to_wire(auto_tier) + params["sessionId"] = self.session_id + await self._client.request("session.model.switchTo", params) + + async def set_auto_tier( + self, auto_tier: AutoTier | _RpcAutoTier | None + ) -> ModelSwitchAutoTierResult: + """ + Change the Auto routing preference without changing the selected model. + + **Experimental.** Part of an experimental Auto routing surface and may + change or be removed in a future release. + + The runtime does not apply the preference immediately. It records the + request and commits it only when a later user turn using the ``auto`` + model successfully obtains a usable model from the provider. A + ``"pending"`` status therefore confirms that the request was accepted, + not that it took effect. + + Watch for the outcome through the ``session.model_change`` event on + success, or the ephemeral ``session.auto_tier_switch_failed`` event on + failure. You can also read the current committed and in-flight state at + any time with ``session.rpc.model.get_current()``. + + Only the most recent request survives: issuing a new request replaces any + earlier one that has not yet been claimed by a turn. + + Args: + auto_tier: Routing preference to activate, or ``None`` to return to + the provider's default Auto routing. + + Returns: + The runtime's immediate acknowledgement and Auto preference snapshot. + + Raises: + Exception: If the session has been destroyed or the connection fails. + + Example: + >>> result = await session.set_auto_tier("intelligence") + >>> if result.status == ModelSwitchAutoTierStatus.PENDING: + ... pass # Takes effect on a later turn that uses the `auto` model. + """ + # `autoTier` is a required field whose null value means "use provider + # default routing", so this cannot go through the generated wrapper, + # which omits null fields. + return ModelSwitchAutoTierResult.from_dict( + await self._client.request( + "session.model.switchAutoTier", + {"sessionId": self.session_id, "autoTier": _auto_tier_to_wire(auto_tier)}, ) ) diff --git a/python/e2e/test_auto_tier_e2e.py b/python/e2e/test_auto_tier_e2e.py new file mode 100644 index 0000000000..5a878c4655 --- /dev/null +++ b/python/e2e/test_auto_tier_e2e.py @@ -0,0 +1,81 @@ +""" +E2E coverage for Auto routing tier switching (snapshot category ``auto_tier``). + +The runtime stages an Auto routing preference instead of applying it immediately: a +request stays "unclaimed" until a later turn using the ``auto`` model mints a usable +model and token pair. These tests observe that staged state through +``model.get_current``, so they assert what the runtime actually recorded rather than +what the SDK serialized. +""" + +from __future__ import annotations + +import pytest + +from copilot.rpc import ModelSwitchAutoTierStatus +from copilot.session import PermissionHandler +from copilot.session_events import AutoTier + +from .testharness import E2ETestContext + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +async def pending_auto_tier(session) -> AutoTier | None: + return (await session.rpc.model.get_current()).pending_auto_tier + + +class TestAutoTier: + async def test_should_stage_and_reset_auto_tier_preference(self, ctx: E2ETestContext): + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="auto", + ) + try: + assert await pending_auto_tier(session) is None + + staged = await session.set_auto_tier("efficiency") + assert staged.status == ModelSwitchAutoTierStatus.PENDING + assert staged.pending_auto_tier == AutoTier.EFFICIENCY + assert await pending_auto_tier(session) == AutoTier.EFFICIENCY + + # A second request replaces the first and reports the one it displaced. + superseded = await session.set_auto_tier("intelligence") + assert superseded.status == ModelSwitchAutoTierStatus.PENDING + assert superseded.pending_auto_tier == AutoTier.INTELLIGENCE + assert superseded.superseded_auto_tier == AutoTier.EFFICIENCY + assert await pending_auto_tier(session) == AutoTier.INTELLIGENCE + + # Passing None returns the session to provider-default routing. The status is + # "unchanged" because provider-default was already the committed preference; + # the request's effect is cancelling the staged one. + reset = await session.set_auto_tier(None) + assert reset.status == ModelSwitchAutoTierStatus.UNCHANGED + assert reset.superseded_auto_tier == AutoTier.INTELLIGENCE + assert await pending_auto_tier(session) is None + finally: + await session.disconnect() + + async def test_should_preserve_auto_tier_when_set_model_omits_it(self, ctx: E2ETestContext): + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="auto", + ) + try: + await session.set_auto_tier("balance") + assert await pending_auto_tier(session) == AutoTier.BALANCE + + # Omitting the argument leaves the staged preference alone. + await session.set_model("auto") + assert await pending_auto_tier(session) == AutoTier.BALANCE + + # Supplying a tier replaces it. + await session.set_model("auto", auto_tier="intelligence") + assert await pending_auto_tier(session) == AutoTier.INTELLIGENCE + + # Supplying None clears it. Omission, a value, and None are three distinct + # outcomes, which is why the argument cannot collapse to a plain optional. + await session.set_model("auto", auto_tier=None) + assert await pending_auto_tier(session) is None + finally: + await session.disconnect() diff --git a/python/test_client.py b/python/test_client.py index e62154e247..47a1aa1f7c 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -6,6 +6,7 @@ import asyncio import inspect +import json import os from datetime import UTC, datetime from tempfile import TemporaryDirectory @@ -21,6 +22,7 @@ ExtensionInfo, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, + ModelSwitchAutoTierStatus, RuntimeConnection, StdioRuntimeConnection, define_tool, @@ -38,6 +40,7 @@ ModelLimits, ModelSupports, ) +from copilot.generated.rpc import AutoTier as AutoTierEnum from copilot.session import PermissionHandler from copilot.session_events import ( McpOauthRequestReason, @@ -2585,6 +2588,135 @@ async def mock_request(method, params, **kwargs): assert captured["session.model.switchTo"]["modelId"] == "gpt-4.1" assert captured["session.model.switchTo"]["reasoningSummary"] == "detailed" assert captured["session.model.switchTo"]["contextTier"] == "long_context" + assert "autoTier" not in captured["session.model.switchTo"] + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_set_model_sends_auto_tier(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.model.switchTo": + return {} + return await original_request(method, params, **kwargs) + + client._client.request = mock_request + await session.set_model("auto", auto_tier="intelligence") + assert captured["session.model.switchTo"]["sessionId"] == session.session_id + assert captured["session.model.switchTo"]["modelId"] == "auto" + assert captured["session.model.switchTo"]["autoTier"] == "intelligence" + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_set_model_sends_explicit_null_auto_tier(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.model.switchTo": + return {} + return await original_request(method, params, **kwargs) + + client._client.request = mock_request + await session.set_model("auto", auto_tier=None) + # An explicit null must survive to the wire; omitting it would mean + # "leave the preference alone" rather than "use default routing". + assert "autoTier" in captured["session.model.switchTo"] + assert captured["session.model.switchTo"]["autoTier"] is None + finally: + await client.force_stop() + + +class TestSetAutoTier: + @pytest.mark.asyncio + @pytest.mark.parametrize("auto_tier", ["efficiency", "balance", "intelligence", None]) + async def test_set_auto_tier_sends_correct_rpc(self, auto_tier): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.model.switchAutoTier": + return { + "status": "pending", + "effectiveAutoTier": "balance", + "pendingAutoTier": auto_tier, + "activatingAutoTier": None, + } + return await original_request(method, params, **kwargs) + + client._client.request = mock_request + result = await session.set_auto_tier(auto_tier) + + params = captured["session.model.switchAutoTier"] + assert params["sessionId"] == session.session_id + assert "autoTier" in params + assert params["autoTier"] == auto_tier + + assert result.status == ModelSwitchAutoTierStatus.PENDING + assert result.effective_auto_tier == AutoTierEnum.BALANCE + assert result.activating_auto_tier is None + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_set_auto_tier_accepts_the_enum_it_returns(self): + """The tier on a result or event is an enum, so it has to be valid input too.""" + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.model.switchAutoTier": + return {"status": "pending", "effectiveAutoTier": "intelligence"} + return await original_request(method, params, **kwargs) + + client._client.request = mock_request + await session.set_auto_tier(AutoTierEnum.INTELLIGENCE) + + params = captured["session.model.switchAutoTier"] + # The value must be a plain string; the JSON-RPC encoder cannot + # serialize an enum. + assert params["autoTier"] == "intelligence" + assert isinstance(params["autoTier"], str) + json.dumps(params) finally: await client.force_stop() diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index a42b9994fc..65e39a80ba 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -15,6 +15,7 @@ from copilot.session_events import ( AttachmentGitHubReferenceType, AutoTier, + AutoTierSwitchFailureReason, Data, ElicitationCompletedAction, ElicitationRequestedMode, @@ -23,6 +24,7 @@ PermissionPromptRequestMemory, PermissionRequestMemory, PermissionRequestMemoryAction, + SessionAutoTierSwitchFailedData, SessionEventType, SessionManagedSettingsResolvedData, SessionResumeData, @@ -71,6 +73,53 @@ def test_auto_tier_lifecycle_events_round_trip(self, event_type, tier): else: assert serialized["autoTier"] == tier + @pytest.mark.parametrize( + "reason", + ["policy_rejected", "request_failed", "setup_failed", "unsupported"], + ) + def test_auto_tier_switch_failed_event_decodes_every_reason(self, reason): + timestamp = "2026-08-28T00:00:00Z" + event = session_event_from_dict( + { + "id": str(uuid4()), + "timestamp": timestamp, + "parentId": None, + "type": "session.auto_tier_switch_failed", + "data": { + "effectiveAutoTier": "balance", + "requestedAutoTier": "intelligence", + "reason": reason, + }, + } + ) + assert isinstance(event.data, SessionAutoTierSwitchFailedData) + assert event.data.reason == AutoTierSwitchFailureReason(reason) + assert event.data.effective_auto_tier == AutoTier.BALANCE + assert event.data.requested_auto_tier == AutoTier.INTELLIGENCE + + def test_auto_tier_switch_failed_event_allows_null_requested_tier(self): + # A null requested tier means the attempt to return to provider-default + # Auto routing is what failed. + timestamp = "2026-08-28T00:00:00Z" + event = session_event_from_dict( + { + "id": str(uuid4()), + "timestamp": timestamp, + "parentId": None, + "type": "session.auto_tier_switch_failed", + "data": { + "effectiveAutoTier": "efficiency", + "requestedAutoTier": None, + "reason": "unsupported", + }, + } + ) + assert isinstance(event.data, SessionAutoTierSwitchFailedData) + assert event.data.requested_auto_tier is None + assert event.data.effective_auto_tier == AutoTier.EFFICIENCY + serialized = session_event_to_dict(event)["data"] + assert serialized["requestedAutoTier"] is None + def test_session_usage_info_is_recognized(self): """The session.usage_info event type should be in the enum.""" assert SessionEventType.SESSION_USAGE_INFO.value == "session.usage_info" diff --git a/rust/README.md b/rust/README.md index a561e6da09..72f9571e70 100644 --- a/rust/README.md +++ b/rust/README.md @@ -380,9 +380,30 @@ let config = SessionConfig::default() The same options work with `ResumeSessionConfig::with_capi` and can be combined with `with_enable_web_socket_responses(false)`. The SDK omits an unset tier: the runtime chooses its default on create and preserves the persisted/current -tier on resume. An explicit tier overrides the persisted tier on cold resume; -the runtime rejects a conflicting tier when the session is already resident -in memory. The SDK does not choose a default or manage tier persistence. +tier on resume. An explicit tier overrides the persisted tier on cold resume. On +resident resume, a different tier requests a safe switch applied after the +resume succeeds; it cannot change a turn that is already in flight. The SDK does not choose a default or manage tier persistence. + +### Changing the Auto tier during a session + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```rust,ignore +use github_copilot_sdk::{AutoTier, ModelSwitchAutoTierStatus}; + +let result = session.set_auto_tier(Some(AutoTier::Intelligence)).await?; +if result.status == ModelSwitchAutoTierStatus::Pending { + // Accepted, but not yet in effect. +} + +// Return to the provider's default Auto routing. +session.set_auto_tier(None).await?; +``` + +`set_model` accepts the same preference through `SetModelOptions::with_auto_tier`, which stages the tier atomically with selecting `auto`. Use `with_reset_auto_tier` instead to return to provider-default routing. + See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the lifecycle rules. diff --git a/rust/src/session.rs b/rust/src/session.rs index 029e640948..7cf207d104 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -14,8 +14,9 @@ use tracing::{Instrument, error, warn}; use crate::canvas::CanvasHandler; use crate::generated::api_types::{ - LogRequest, ModelSwitchToRequest, OpenCanvasInstance, PermissionDecisionRequest, - RegisterEventInterestParams, ToolsGetCurrentMetadataResult, rpc_methods, + LogRequest, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, ModelSwitchToRequest, + OpenCanvasInstance, PermissionDecisionRequest, RegisterEventInterestParams, + ToolsGetCurrentMetadataResult, rpc_methods, }; use crate::generated::session_events::{ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData, @@ -32,12 +33,12 @@ use crate::session_fs::SessionFsProvider; use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; use crate::types::{ - CommandContext, CommandDefinition, CommandHandler, CreateSessionResult, ElicitationRequest, - ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions, - PermissionRequestData, RequestId, ResumeSessionConfig, ResumeSessionResult, SectionOverride, - SessionCapabilities, SessionConfig, SessionEvent, SessionId, SetModelOptions, - SystemMessageConfig, ToolInvocation, ToolResult, ToolResultExpanded, TraceContext, - UiInputOptions, ensure_attachment_display_names, + AutoTier, AutoTierPreference, CommandContext, CommandDefinition, CommandHandler, + CreateSessionResult, ElicitationRequest, ElicitationResult, ExitPlanModeData, + GetMessagesResponse, MessageOptions, PermissionRequestData, RequestId, ResumeSessionConfig, + ResumeSessionResult, SectionOverride, SessionCapabilities, SessionConfig, SessionEvent, + SessionId, SetModelOptions, SystemMessageConfig, ToolInvocation, ToolResult, + ToolResultExpanded, TraceContext, UiInputOptions, ensure_attachment_display_names, }; use crate::{ Client, Error, ErrorKind, JsonRpcResponse, SessionErrorKind, SessionEventNotification, @@ -548,8 +549,12 @@ impl Session { /// Pass `None` for `opts` if no extra configuration is needed. pub async fn set_model(&self, model: &str, opts: Option) -> Result<(), Error> { let opts = opts.unwrap_or_default(); + let auto_tier = opts.auto_tier.clone(); let request = ModelSwitchToRequest { - auto_tier: None, + auto_tier: match &auto_tier { + Some(AutoTierPreference::Tier(tier)) => Some(tier.clone()), + _ => None, + }, compaction_decision: None, context_tier: opts.context_tier, defer_if_model_change_queued: None, @@ -565,10 +570,65 @@ impl Session { source: None, verbosity: None, }; + + if matches!(auto_tier, Some(AutoTierPreference::Reset)) { + // The generated request skips a `None` tier, which the runtime reads + // as "leave the preference alone" rather than "use provider-default + // routing", so send an explicit null instead. + let mut wire_params = serde_json::to_value(request)?; + wire_params["sessionId"] = serde_json::Value::String(self.id.to_string()); + wire_params["autoTier"] = serde_json::Value::Null; + self.client + .call("session.model.switchTo", Some(wire_params)) + .await?; + return Ok(()); + } + self.rpc().model().switch_to(request).await?; Ok(()) } + /// Change the Auto routing preference without changing the selected model. + /// + /// The runtime does not apply the preference immediately. It records the + /// request and commits it only when a later user turn using the `auto` + /// model successfully obtains a usable model from the provider. A + /// [`ModelSwitchAutoTierStatus::Pending`] status therefore confirms that the + /// request was accepted, not that it took effect. + /// + /// Watch for the outcome through the `session.model_change` event on + /// success, or the ephemeral `session.auto_tier_switch_failed` event on + /// failure. You can also read the current committed and in-flight state at + /// any time through `session.rpc().model().get_current()`. + /// + /// Only the most recent request survives: issuing a new request replaces any + /// earlier one that has not yet been claimed by a turn. + /// + /// Pass `None` to return to the provider's default Auto routing. + /// + /// **Experimental.** Part of an experimental Auto routing surface and may + /// change or be removed in a future release. + /// + /// # Cancel safety + /// + /// **Cancel-safe.** Single `session.model.switchAutoTier` RPC; the + /// underlying [`Client::call`](crate::Client::call) is cancel-safe via the + /// writer-actor. + /// + /// [`ModelSwitchAutoTierStatus::Pending`]: crate::generated::api_types::ModelSwitchAutoTierStatus::Pending + pub async fn set_auto_tier( + &self, + auto_tier: Option, + ) -> Result { + self.rpc() + .model() + .switch_auto_tier(ModelSwitchAutoTierRequest { + auto_tier, + source: None, + }) + .await + } + /// Disconnect this session from the CLI. /// /// Sends the `session.destroy` RPC, stops the event loop, and unregisters diff --git a/rust/src/types.rs b/rust/src/types.rs index ee3ac3df26..32b504e336 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -21,6 +21,8 @@ pub use crate::copilot_request_handler::{ CopilotWebSocketResponse, WebSocketTransform, forward_http, }; use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance}; +/// Acknowledgement and Auto preference snapshot returned by an Auto tier switch. +pub use crate::generated::api_types::{ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus}; /// Routing tier for the `auto` model with Auto mode V2. pub use crate::generated::session_events::AutoTier; use crate::generated::session_events::ReasoningSummary; @@ -1422,10 +1424,13 @@ pub struct CapiSessionOptions { /// Routing tier, meaningful only with model `auto` (Auto mode V2). /// Requires a runtime version that supports `capi.autoTier`. /// - /// When omitted, the runtime chooses its default on create and preserves - /// the persisted or current tier on resume. An explicit tier overrides the - /// persisted tier on cold resume; the runtime rejects a conflicting tier - /// when resuming a session already resident in memory. + /// When omitted, the runtime chooses its default on create and restores + /// the last committed tier on cold resume. On resident resume, a different + /// tier requests a safe switch that takes effect after resume succeeds and + /// never disturbs a turn that is already running. + /// + /// To change the preference on a live session, use + /// [`Session::set_auto_tier`](crate::session::Session::set_auto_tier). #[serde(default, skip_serializing_if = "Option::is_none")] pub auto_tier: Option, @@ -4791,6 +4796,30 @@ pub struct SetModelOptions { /// fields set on the override are applied; the rest fall back to the /// runtime-resolved values for the model. pub model_capabilities: Option, + /// Auto routing preference to stage atomically with selecting the `auto` + /// model. + /// + /// Leave as `None` to leave the current preference alone. The runtime + /// rejects this option when the model is anything other than `auto`; use + /// [`Session::set_auto_tier`](crate::session::Session::set_auto_tier) to + /// change the preference without changing the selected model. + pub auto_tier: Option, +} + +/// Auto routing preference requested alongside a model switch. +/// +/// **Experimental.** Part of an experimental Auto routing surface and may change +/// or be removed in a future release. +/// +/// This is a three-state choice. Leaving [`SetModelOptions::auto_tier`] as +/// `None` leaves the current preference alone, which is different from +/// [`AutoTierPreference::Reset`], which actively resets it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AutoTierPreference { + /// Route using a specific tier. + Tier(AutoTier), + /// Return to the provider's default Auto routing. + Reset, } impl SetModelOptions { @@ -4820,6 +4849,19 @@ impl SetModelOptions { self.model_capabilities = Some(caps); self } + + /// Set [`auto_tier`](Self::auto_tier) to a specific routing tier. + pub fn with_auto_tier(mut self, tier: AutoTier) -> Self { + self.auto_tier = Some(AutoTierPreference::Tier(tier)); + self + } + + /// Set [`auto_tier`](Self::auto_tier) to return to the provider's default + /// Auto routing. + pub fn with_reset_auto_tier(mut self) -> Self { + self.auto_tier = Some(AutoTierPreference::Reset); + self + } } /// Response from the top-level `ping` RPC. diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index 942c5dab5b..9ddd450e3f 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -3,15 +3,16 @@ #![allow(clippy::unwrap_used)] -use github_copilot_sdk::AutoTier; use github_copilot_sdk::rpc::{ Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, - ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, QueuePendingItems, - QueuePendingItemsKind, SendAgentMode, TasksStartAgentRequest, + ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, ModelSwitchAutoTierRequest, + ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, + SendAgentMode, TasksStartAgentRequest, }; use github_copilot_sdk::session_events::{ PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent, }; +use github_copilot_sdk::{AutoTier, AutoTierPreference, SetModelOptions}; #[test] fn session_events_deserialize_auto_tier() { @@ -197,3 +198,65 @@ fn running_extension(id: &str, name: &str) -> Extension { status: ExtensionStatus::Running, } } + +#[test] +fn switch_auto_tier_request_serializes_explicit_null_tier() { + // `autoTier` is a required field whose null value means "use provider-default + // routing", so it must survive serialization rather than being skipped. + let request = ModelSwitchAutoTierRequest { + auto_tier: None, + source: None, + }; + let wire = serde_json::to_value(&request).unwrap(); + + assert_eq!(wire.get("autoTier"), Some(&serde_json::Value::Null)); + assert!(wire.get("source").is_none()); +} + +#[test] +fn switch_auto_tier_request_serializes_each_tier() { + for (tier, expected) in [ + (AutoTier::Efficiency, "efficiency"), + (AutoTier::Balance, "balance"), + (AutoTier::Intelligence, "intelligence"), + ] { + let request = ModelSwitchAutoTierRequest { + auto_tier: Some(tier), + source: None, + }; + let wire = serde_json::to_value(&request).unwrap(); + assert_eq!(wire["autoTier"], serde_json::json!(expected)); + } +} + +#[test] +fn switch_auto_tier_result_deserializes_full_snapshot() { + let result: ModelSwitchAutoTierResult = serde_json::from_value(serde_json::json!({ + "status": "pending", + "effectiveAutoTier": "balance", + "pendingAutoTier": "intelligence", + "activatingAutoTier": null, + "supersededAutoTier": null + })) + .unwrap(); + + assert_eq!(result.status, ModelSwitchAutoTierStatus::Pending); + assert_eq!(result.effective_auto_tier, Some(AutoTier::Balance)); + assert_eq!(result.pending_auto_tier, Some(AutoTier::Intelligence)); + assert_eq!(result.activating_auto_tier, None); +} + +#[test] +fn set_model_options_distinguishes_unset_tier_from_reset() { + let untouched = SetModelOptions::default(); + assert_eq!(untouched.auto_tier, None); + + let explicit = SetModelOptions::default().with_auto_tier(AutoTier::Intelligence); + assert_eq!( + explicit.auto_tier, + Some(AutoTierPreference::Tier(AutoTier::Intelligence)) + ); + + let cleared = SetModelOptions::default().with_reset_auto_tier(); + assert_eq!(cleared.auto_tier, Some(AutoTierPreference::Reset)); +} diff --git a/rust/tests/e2e.rs b/rust/tests/e2e.rs index 03723dfb1b..eb4e750990 100644 --- a/rust/tests/e2e.rs +++ b/rust/tests/e2e.rs @@ -5,6 +5,8 @@ mod abort; #[path = "e2e/ask_user.rs"] mod ask_user; +#[path = "e2e/auto_tier.rs"] +mod auto_tier; #[path = "e2e/builtin_tools.rs"] mod builtin_tools; #[path = "e2e/byok_bearer_token_provider.rs"] diff --git a/rust/tests/e2e/auto_tier.rs b/rust/tests/e2e/auto_tier.rs new file mode 100644 index 0000000000..85c70dd460 --- /dev/null +++ b/rust/tests/e2e/auto_tier.rs @@ -0,0 +1,140 @@ +use github_copilot_sdk::SetModelOptions; +use github_copilot_sdk::rpc::ModelSwitchAutoTierStatus; +use github_copilot_sdk::session::Session; +use github_copilot_sdk::session_events::AutoTier; + +use super::support::with_dedicated_e2e_context; + +const MODEL_ID: &str = "auto"; + +/// End-to-end coverage for staging and resetting an Auto routing preference +/// (snapshot category "auto_tier"). +/// +/// The runtime stages an Auto routing preference instead of applying it immediately: a +/// request stays unclaimed until a later turn using the `auto` model mints a usable model +/// and token pair. These tests observe that staged state through `model().get_current()`, +/// so they assert what the runtime actually recorded rather than what the SDK serialized. +async fn pending_auto_tier(session: &Session) -> Option { + session + .rpc() + .model() + .get_current() + .await + .expect("get current model") + .pending_auto_tier +} + +#[tokio::test] +async fn should_stage_and_reset_auto_tier_preference() { + with_dedicated_e2e_context( + "auto_tier", + "should_stage_and_reset_auto_tier_preference", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let session = client + .create_session(ctx.approve_all_session_config().with_model(MODEL_ID)) + .await + .expect("create session"); + + assert_eq!(pending_auto_tier(&session).await, None); + + let staged = session + .set_auto_tier(Some(AutoTier::Efficiency)) + .await + .expect("stage efficiency"); + assert_eq!(staged.status, ModelSwitchAutoTierStatus::Pending); + assert_eq!(staged.pending_auto_tier, Some(AutoTier::Efficiency)); + assert_eq!( + pending_auto_tier(&session).await, + Some(AutoTier::Efficiency) + ); + + // A second request replaces the first and reports the one it displaced. + let superseded = session + .set_auto_tier(Some(AutoTier::Intelligence)) + .await + .expect("stage intelligence"); + assert_eq!(superseded.status, ModelSwitchAutoTierStatus::Pending); + assert_eq!(superseded.pending_auto_tier, Some(AutoTier::Intelligence)); + assert_eq!(superseded.superseded_auto_tier, Some(AutoTier::Efficiency)); + assert_eq!( + pending_auto_tier(&session).await, + Some(AutoTier::Intelligence) + ); + + // `None` returns the session to provider-default routing. The status is + // `Unchanged` because provider-default was already the committed + // preference; the request's effect is cancelling the staged one. + let reset = session.set_auto_tier(None).await.expect("reset tier"); + assert_eq!(reset.status, ModelSwitchAutoTierStatus::Unchanged); + assert_eq!(reset.superseded_auto_tier, Some(AutoTier::Intelligence)); + assert_eq!(pending_auto_tier(&session).await, None); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_preserve_auto_tier_when_set_model_omits_it() { + with_dedicated_e2e_context( + "auto_tier", + "should_preserve_auto_tier_when_set_model_omits_it", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let session = client + .create_session(ctx.approve_all_session_config().with_model(MODEL_ID)) + .await + .expect("create session"); + + session + .set_auto_tier(Some(AutoTier::Balance)) + .await + .expect("stage balance"); + assert_eq!(pending_auto_tier(&session).await, Some(AutoTier::Balance)); + + // Omitting the preference leaves the staged one alone. + session + .set_model(MODEL_ID, None) + .await + .expect("set model without a tier"); + assert_eq!(pending_auto_tier(&session).await, Some(AutoTier::Balance)); + + // Supplying a tier replaces it. + session + .set_model( + MODEL_ID, + Some(SetModelOptions::default().with_auto_tier(AutoTier::Intelligence)), + ) + .await + .expect("set model with a tier"); + assert_eq!( + pending_auto_tier(&session).await, + Some(AutoTier::Intelligence) + ); + + // Requesting a reset clears it. Omission, a tier, and a reset are three + // distinct outcomes, which `AutoTierPreference` makes explicit. + session + .set_model( + MODEL_ID, + Some(SetModelOptions::default().with_reset_auto_tier()), + ) + .await + .expect("set model with a reset"); + assert_eq!(pending_auto_tier(&session).await, None); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} diff --git a/test/snapshots/auto_tier/should_preserve_auto_tier_when_set_model_omits_it.yaml b/test/snapshots/auto_tier/should_preserve_auto_tier_when_set_model_omits_it.yaml new file mode 100644 index 0000000000..b287603f41 --- /dev/null +++ b/test/snapshots/auto_tier/should_preserve_auto_tier_when_set_model_omits_it.yaml @@ -0,0 +1,4 @@ +models: + - auto + - claude-sonnet-5 +conversations: [] diff --git a/test/snapshots/auto_tier/should_stage_and_reset_auto_tier_preference.yaml b/test/snapshots/auto_tier/should_stage_and_reset_auto_tier_preference.yaml new file mode 100644 index 0000000000..b287603f41 --- /dev/null +++ b/test/snapshots/auto_tier/should_stage_and_reset_auto_tier_preference.yaml @@ -0,0 +1,4 @@ +models: + - auto + - claude-sonnet-5 +conversations: [] From d5f95dfba2f492a41c69a8f952d1eb151141e73f Mon Sep 17 00:00:00 2001 From: Rince Yuan Date: Fri, 4 Sep 2026 19:26:48 +0800 Subject: [PATCH 56/60] docs: link runtime prerequisites instead of restating versions (#2518) The getting-started prerequisites listed 'Node.js 20+', but the Node SDK requires '^20.19.0 || >=22.12.0', so 20.0-20.18 and all of 21.x fail to install. nodejs/README.md already states the real constraint. Point each language at the Prerequisites section of its own README, matching how docs/setup/bundled-cli.md now refers to them, so there is one place per SDK to keep current. Co-authored-by: Zhangyi Yuan --- docs/getting-started.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 53b6497fdb..5512da8531 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -19,8 +19,7 @@ Copilot: In Tokyo it's 75°F and sunny. Great day to be outside! Before you begin, make sure you have: * **GitHub Copilot CLI** installed and authenticated (the Node.js, Python, and .NET SDKs provide the CLI automatically—see [Bundled CLI](./setup/bundled-cli.md). Required for Go, Java, and Rust unless using their application-level CLI bundling features.) -* Your preferred language runtime: - * **Node.js** 20+ or **Python** 3.11+ or **Go** 1.24+ or **Rust** 1.94+ or **Java** 17+ or **.NET** 8.0+ +* Your preferred language runtime, at or above the version its SDK requires. Each SDK states its own floor in the Prerequisites section of its README: [Node.js](../nodejs/README.md#prerequisites), [Python](../python/README.md#prerequisites), [Go](../go/README.md#prerequisites), [Rust](../rust/README.md#prerequisites), [Java](../java/README.md#prerequisites), [.NET](../dotnet/README.md#prerequisites). Verify the CLI is working: From de75efa95561b7b951b78eaf80f40c3c2839c6a7 Mon Sep 17 00:00:00 2001 From: Rince Yuan Date: Fri, 4 Sep 2026 19:27:02 +0800 Subject: [PATCH 57/60] docs(rust): point dependency examples at the published 1.0 line (#2520) * docs(rust): point dependency examples at the published 1.0 line The crate has been on crates.io since 1.0.0 (2026-06-02) and is currently 1.0.11 stable, but the README still pinned `version = "0.1"` and carried a caveat saying the crate was not published yet. 0.1.0 (2026-05-06) predates the feature rename, so it exposes `embedded-cli` rather than `bundled-cli` / `bundled-in-process`. Copying the README's own feature examples therefore fails to resolve: error: failed to select a version for `github-copilot-sdk` The bare pin does resolve, but silently to the four-month-old 0.1.0. With `version = "1.0"` the same examples resolve to 1.0.11. * docs(rust): use compatible major version requirements Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Zhangyi Yuan Co-authored-by: Steve Sanderson Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/rust/README.md b/rust/README.md index 72f9571e70..732b534b5d 100644 --- a/rust/README.md +++ b/rust/README.md @@ -949,7 +949,7 @@ Enable `bundled-in-process` to additionally embed the native runtime library and use `Transport::InProcess`: ```toml -github-copilot-sdk = { version = "0.1", features = ["bundled-in-process"] } +github-copilot-sdk = { version = "1", features = ["bundled-in-process"] } ``` `CliProgram::Path` and raw `ClientOptions::extra_args` apply only to @@ -959,7 +959,7 @@ provisioned compatible runtime package with in-process transport. For builds that prefer a smaller artifact, disable the `bundled-cli` feature: ```toml -github-copilot-sdk = { version = "0.1", default-features = false } +github-copilot-sdk = { version = "1", default-features = false } ``` > **You become responsible for supplying the runtime at deployment.** With @@ -1097,20 +1097,17 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64` | `derive` | — | `schema_for::()` for generating JSON Schema from Rust types (adds `schemars`). | ```toml -# These examples use registry syntax for illustration; until the crate is -# published, use a path or git dependency instead. - # Default — bundles the Copilot CLI in your binary. -github-copilot-sdk = "0.1" +github-copilot-sdk = "1" # Enable the in-process transport and bundle its native runtime library. -github-copilot-sdk = { version = "0.1", features = ["bundled-in-process"] } +github-copilot-sdk = { version = "1", features = ["bundled-in-process"] } # Opt out of bundling — supply the CLI explicitly at runtime. -github-copilot-sdk = { version = "0.1", default-features = false } +github-copilot-sdk = { version = "1", default-features = false } # Derive JSON Schema for tool parameters (adds to default bundled-cli). -github-copilot-sdk = { version = "0.1", features = ["derive"] } +github-copilot-sdk = { version = "1", features = ["derive"] } ``` ## Development From 8f14e9a6123a621c88e5c67be53eb597c056f843 Mon Sep 17 00:00:00 2001 From: Rince Yuan Date: Fri, 4 Sep 2026 19:27:17 +0800 Subject: [PATCH 58/60] docs(java): fix the installation section structure (#2521) Two sentences told the reader to replace `${copilot.sdk.version}`, but no snippet contains that placeholder - the release automation writes concrete versions in. The copy under the snapshot Gradle example additionally said to use "the latest release from Maven Central" while showing a -SNAPSHOT. `Snapshot Builds` was also nested under `### Gradle` even though it covers both build tools, and the snapshot Gradle example sat at `###`, so the page rendered two sibling "Gradle" installation sections with conflicting versions (and a duplicate `gradle` heading slug). Promote the section to `### Snapshot builds` with `#### Maven` / `#### Gradle` underneath, and drop the stale sentences. No version string changes, so scripts/update-documentation-versions.sh keeps its one-release-and-one-snapshot contract for both build tools. Co-authored-by: Zhangyi Yuan --- java/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/java/README.md b/java/README.md index 6a95da424e..405e3bd379 100644 --- a/java/README.md +++ b/java/README.md @@ -30,8 +30,6 @@ runtime. ### Maven -Replace `${copilot.sdk.version}` with the latest release from Maven Central. - ```xml com.github @@ -46,9 +44,11 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. implementation 'com.github:copilot-sdk-java:1.0.13-preview.5' ``` -#### Snapshot Builds +### Snapshot builds + +Snapshot builds of the next development version are published to Maven Central Snapshots. To use them, add the snapshot repository and depend on the development version: -Snapshot builds of the next development version are published to Maven Central Snapshots. To use them, add the repository and update the dependency version in your `pom.xml`: +#### Maven ```xml @@ -66,9 +66,7 @@ Snapshot builds of the next development version are published to Maven Central S ``` -### Gradle - -Replace `${copilot.sdk.version}` with the latest release from Maven Central. +#### Gradle ```groovy implementation 'com.github:copilot-sdk-java:1.0.14-preview.5-SNAPSHOT' From eb38014b8293cb93647dc81212f94a85bed950a8 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 4 Sep 2026 04:27:53 -0700 Subject: [PATCH 59/60] [Rust] Add PreparedSession for loss-free startup event subscription (#2319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Rust] Add PreparedSession for loss-free startup event subscription `Session::subscribe()` can only be called once the session handle exists, so every event the runtime broadcast during `session.create` / `session.resume` had no receiver installed and was dropped. Ephemeral events like `session.idle` are never written to the session log, so `get_messages` cannot recover them afterwards either. `Client::prepare_session` / `prepare_resume_session` return a `PreparedSession` that owns the session's broadcast channel up front: subscribe first, then `start()`. `prepare_*` is synchronous and inert — it validates the event buffer capacity, allocates a local channel and cancellation token, and performs no router registration, task spawn, or wire activity until `start()` is first polled. `start(self)` consumes the handle and the type is deliberately not `Clone`, so a prepared session can never produce two event loops. `create_session` / `resume_session` become wrappers over `prepare_*(config)?.start().await`, preserving their RPC sequences and error kinds. Their bodies moved into private start paths that take the sender and token by injection rather than allocating their own. Both configs gain a runtime-only `event_buffer_capacity` (default 512, `Some(0)` rejected as `InvalidConfig`, never clamped). The buffer is finite, so slow subscribers observe `Lagged` instead of applying backpressure. Cancellation cleanup is now symmetric. `PendingSessionRegistration` grew a deferred variant that resolves the session ID from the inline-response stash, so the create path — including the cloud server-assigned-ID path, which previously had no RAII guard at all — unregisters and cancels when the startup future is dropped or fails. Registration and stashing now happen under one lock hold to close the window where a concurrent drop would miss a just-registered session. The mcp-auth-interest error path on both create and resume now cancels and awaits the event loop instead of returning through `?`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * [Rust] Close two session-registration cancellation races Follow-up to the `PreparedSession` change. Two cancellation races remained in session registration, both reachable from a caller simply dropping a `start()` future. **Deferred cloud-create registration.** For a cloud session with no caller-pinned ID the CLI assigns the ID, so the SDK can only register on the notification router from the inline `session.create` response callback. The read loop removes the pending-response entry *before* invoking that callback, so a startup future dropped in that window found an empty stash, cleaned up nothing, and the callback then registered a session with no owner — a permanent router leak. Registration state now lives in a shared `DeferredRegistration` slot (`Pending` / `Registered` / `Cancelled` / `Claimed`) that the callback, the startup path, and the cancellation guard all arbitrate through. The callback registers *under the slot lock*, so registering and publishing ownership are atomic with respect to cancellation: a concurrent guard either wins and marks the slot `Cancelled`, in which case the callback registers nothing, or it loses and finds a `Registered` slot to tear down. Never both, and never neither. The pinned-ID path uses the same slot, pre-populated, so create has one cleanup mechanism instead of two. **Stale cleanup versus a same-ID retry.** Unregistering by session ID alone removed whichever registration happened to hold the ID. Because cleanup of an abandoned startup is signalled rather than awaited, a caller that aborted a startup and immediately retried with the same pinned ID could have the retry's registration evicted by the dead attempt, silently stranding the live session with no event routing. The same applied to a `Session` dropped after being superseded. Registrations now carry a `RegistrationToken` identity and removal is a compare-and-remove: an owner removes only the exact registration it registered. Applied to create, resume, `Session::disconnect`, and `Session::drop`. `Client::stop` and `cleanup_sessions_for_test` keep removing unconditionally — they tear down every session and the runtime regardless of owner. Tests gate both windows deterministically rather than by timing. The slot state machine is driven directly at the exact interleaving the read loop creates, in both orders, and the router's compare-and-remove is covered on its own. End to end: a cancelled cloud create leaves no registration, no subscription, and no task behind whether cancellation lands before or after the callback registered, and a same-ID retry still succeeds; and create, resume, and `Session` drop each survive a stale owner's cleanup running after a retry has taken over the ID. Each test was confirmed to fail against a mutated implementation. No public API change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Gate registered_session_ids to test configurations `Client::registered_session_ids` has no caller in a default-feature build: the in-crate unit tests reach it under `cfg(test)`, and the public `registered_session_ids_for_test` wrapper is gated on `feature = "test-support"`. A plain `cargo build` or `cargo clippy` therefore warned `dead_code` for it. Gate the method on `any(test, feature = "test-support")`, matching the convention already used for the other test-only helpers in this file. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Narrow the PreparedSession guarantee to routed events `Client::prepare_session` promised consumers would observe "every event a session emits". That is broader than the implementation for cloud creates with a server-assigned ID: the SDK cannot register the session on its notification router until the `session.create` response arrives, so notifications emitted before that point are not routable to any session and never reach a subscriber. Qualify the primary API documentation and the changelog as *routed* events, and point callers at pinning `SessionConfig::session_id` for complete pre-response coverage. `PreparedSession`'s type-level docs, `rust/README.md`, and `docs/features/streaming-events.md` already documented this limitation; the entry-point docs now match them. Documentation only: no API, behavior, or wire change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Docs] Unwrap the lone Rust tab in the streaming-events article The "Subscribing before a session starts" section has only a Rust example, and the docs normalization pipeline converts a `

` group into a tabbed language switcher only when two or more consecutive blocks are present. A single block renders as raw collapsible HTML on docs.github.com. Drop the `
`/`` wrapper and leave the code fence directly in the article, matching the repository docs style guide. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Keep session IDs out of prepared-session test diagnostics Two polling helpers formatted session identifiers into their failure messages: `await_no_registrations` rendered the router's registered ID list with `{:?}`, and `await_registered` interpolated the awaited ID. A downstream consumer that vendors this crate has CodeQL rules flagging identifiers reaching formatted output, so both were reported there even though the SDK's own analysis was clean. Report an outstanding-registration count and a static expectation message instead. Both helpers keep their exact predicates and deadline behavior: `await_no_registrations` still returns only when the router holds zero registrations, and `await_registered` still blocks on the exact ID it was given, so no assertion is weakened. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Cover the MCP-auth interest failure path on create and resume `session.eventLog.registerInterest` is the last fallible step of startup when an MCP-auth handler is installed, and its failure branch was untested: the existing MCP-auth tests only return successful interest responses, and the prepared-session failure tests covered create RPC errors and ID mismatches only. Add a failing-interest test for each of create and resume, asserting the same contract the sibling failure tests assert: the original error kind reaches the caller, the router registration is gone, and a subscription installed before `start()` is closed. The resume test also asserts the best-effort `session.skills.reload` is never issued, since interest registration runs ahead of it. Verified by mutation that both tests execute the branch. Removing the `registration.cleanup(event_loop)` call does not turn them red, because `PendingSessionRegistration::drop` cancels and releases the same registration synchronously — the explicit cleanup is defense in depth on this path, and the tests assert the observable contract rather than which of the two mechanisms performed it. Test-only change: no API, behavior, or wire impact. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Reconcile the subscribe docs with the buffering contract `PreparedSession::subscribe` promised that "every subscriber receives every event", which contradicts the paragraph directly above it: the broadcast buffer is finite, so a subscriber that falls behind the configured capacity observes `Lagged` and skips events instead of applying backpressure. Say that explicitly and link the `Lagged` type. Also replace "should" with "must" where the streaming-events article and the changelog describe what a consumer needing lossless startup delivery has to do. The docs style guide reserves ambiguous modals for optional actions, and `rust/README.md` already phrased this as "must". Documentation only: no API, behavior, or wire change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Reconcile PreparedSession with current main Preserve current router registration ownership and feature configuration while rebasing the focused startup subscription change.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [Rust] Format rebased session startup code Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [Rust] Avoid exposing session IDs in diagnostics Use a count-only test helper for the polling diagnostic so CodeQL does not treat session identifiers as logged data.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [Rust] Keep session IDs out of test diagnostics CodeQL flagged the registration polling helper because the count was derived from a Vec. Count registrations directly on the router instead, and assert on registration identity without formatting IDs into failure messages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 --- CHANGELOG.md | 22 + docs/features/streaming-events.md | 42 + rust/Cargo.toml | 3 + rust/README.md | 38 +- rust/src/lib.rs | 60 +- rust/src/router.rs | 106 ++- rust/src/session.rs | 498 +++++++++-- rust/src/types.rs | 47 + rust/tests/prepared_session_test.rs | 1242 +++++++++++++++++++++++++++ 9 files changed, 1996 insertions(+), 62 deletions(-) create mode 100644 rust/tests/prepared_session_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e753beda..59961b4f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,28 @@ const session = await joinSession({ const token = process.env.GITHUB_TOKEN; ``` +### Feature: early session-event subscription (Rust) + +The Rust SDK can now observe every event routed to a session, starting with that session's very first routed event. `Client::prepare_session` and `Client::prepare_resume_session` return an inert `PreparedSession` that owns the session's event channel, so a subscription can be installed *before* any protocol activity begins: + +```rust +let prepared = client.prepare_session( + SessionConfig::default().with_event_buffer_capacity(2048), +)?; +let mut events = prepared.subscribe(); +let session = prepared.start().await?; +``` + +Previously, `Session::subscribe` could only be called on the returned session, so events the runtime emitted while `session.create` / `session.resume` was still in flight were broadcast with no receiver installed and dropped. Ephemeral events such as `session.idle` are not persisted, so they could not be recovered with `getMessages` either. + +The guarantee is scoped to *routed* events. For cloud sessions where the server assigns the session ID, the SDK cannot route notifications until the `session.create` response arrives and the ID is known, so events emitted before that point are not routable to any session. Pin `session_id` on the config to get router registration before the RPC, and with it complete pre-response coverage. + +`prepare_*` is synchronous and inert: it validates the buffer capacity and allocates a local channel, and performs no router registration, task spawn, or wire activity until `start()` is first polled. `start(self)` consumes the handle and `PreparedSession` is not `Clone`, so a prepared session can never produce two event loops. Dropping an unstarted handle leaves no state behind; dropping a polled `start()` future cancels the startup and unregisters the session, so a retry with the same session ID succeeds. Session registrations now carry an ownership identity, so cleanup removes only the exact registration it owns and an abandoned startup can never evict a same-ID retry (or a session that replaced it). + +Both `SessionConfig` and `ResumeSessionConfig` gained a runtime-only `event_buffer_capacity` option (default 512, `Some(0)` rejected as an invalid config). The buffer is finite, so slow subscribers observe `Lagged` rather than applying backpressure; consumers that need a lossless view of a large startup burst must size the buffer accordingly or drain concurrently with `start()`. + +`create_session` and `resume_session` are unchanged wrappers over `prepare_*(...)?.start()` with identical RPC sequences and error kinds. + ### Feature: host-injected managed settings permissions Session create and resume accept a new optional `managedSettings` option that injects an enterprise permissions policy at session startup, alongside the existing `enableManagedSettings` self-fetch flag. The current contract is permissions-only: `disableBypassPermissionsMode` (the literal `"disable"`), plus `deny`, `ask`, and `allow` rule lists. The layer composes restrictively with any server- or device-level managed settings (deny/ask are unioned, every present allow list must admit a tool, and `disableBypassPermissionsMode` is deny-wins). diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 10f111d9f9..0292f98e00 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -218,6 +218,48 @@ session.on(AssistantMessageDeltaEvent.class, event -> > [!TIP] > **(TypeScript)** The TypeScript SDK uses a discriminated union—when you match on `event.type`, the `data` payload is automatically narrowed to the correct shape. +## Subscribing before a session starts + +A session can emit events before its create or resume call returns. The agent may already be working—especially on resume with `continuePendingWork`—and ephemeral events such as `session.idle` are never written to the session log, so `getMessages` cannot recover them afterwards. A subscription installed after the session handle exists misses that startup window. + +> [!TIP] +> **(Rust)** `Client::prepare_session` and `Client::prepare_resume_session` return a `PreparedSession` that owns the session's event channel before any protocol activity happens. Subscribe first, then call `start()`. + +```rust +use github_copilot_sdk::{Client, SessionConfig}; + +async fn create_without_missing_startup_events( + client: &Client, +) -> Result<(), github_copilot_sdk::Error> { + let prepared = client.prepare_session( + SessionConfig::default().with_event_buffer_capacity(2048), + )?; + + // Installed before any wire activity: nothing is dropped for lack of a receiver. + let mut events = prepared.subscribe(); + tokio::spawn(async move { + while let Ok(event) = events.recv().await { + println!("{}", event.event_type); + } + }); + + let session = prepared.start().await?; + let _ = session; + Ok(()) +} +``` + +`prepare_*` is synchronous and inert: it validates the buffer capacity, allocates a local channel, and does nothing else. No session is registered and nothing reaches the CLI until `start()` is first polled. Dropping a prepared session that was never started leaves no state behind and closes its subscriptions; dropping the `start()` future cancels the in-flight startup and unregisters the session, so a retry with the same session ID succeeds. Cleanup is scoped to the exact registration the abandoned startup owned, so it cannot evict a retry that has already taken over the same session ID. + +Startup buffering is worth planning for: + +* The event buffer is finite—512 events unless `event_buffer_capacity` overrides it. A capacity of `0` is rejected with an invalid-config error rather than clamped. +* Slow subscribers observe a `Lagged` error reporting how many events were skipped. They never apply backpressure to the session's event loop. +* Consumers that need a lossless view of a large startup burst must either configure a capacity that covers it or drain the subscription concurrently with `start()`. + +> [!NOTE] +> For cloud sessions where the server assigns the session ID, the SDK cannot route notifications until the create response arrives and the ID is known. Events emitted before that point are not routable to any session. The guarantee is narrower: routed events are never dropped for lack of an installed receiver. Pin `session_id` on the config to get routing—and full pre-response coverage—from the first byte. + ## Render only the parent agent response Sub-agent events share the parent session stream and include envelope-level `agentId`. Root/main agent events and session-level events omit `agentId`, so main-chat renderers can ignore assistant events where `agentId` is set and route those events to traces or progress UI instead. diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c66480d511..4495d3928c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -118,6 +118,9 @@ required-features = ["test-support"] test = false bench = false +[[test]] +name = "prepared_session_test" +required-features = ["test-support"] [build-dependencies] dirs = "5" flate2 = "1" diff --git a/rust/README.md b/rust/README.md index 732b534b5d..f236fc5b02 100644 --- a/rust/README.md +++ b/rust/README.md @@ -717,6 +717,36 @@ while let Ok(event) = events.recv().await { When streaming is off (the default), only the final `assistant.message` and `assistant.reasoning` events fire. Delta events arrive in order; concatenating their `delta` text payloads reproduces the final message. +#### Subscribing before the session starts + +`session.subscribe()` can only be called once the session exists, so any event the runtime emits while `session.create` / `session.resume` is still in flight is broadcast with no receiver installed and is not delivered. Ephemeral events such as `session.idle` are not written to the session log either, so `get_messages` can't recover them afterwards. + +`Client::prepare_session` / `Client::prepare_resume_session` close that window. They return a `PreparedSession` that owns the session's broadcast channel up front: + +```rust,ignore +let prepared = client.prepare_session( + SessionConfig::default().with_event_buffer_capacity(2048), +)?; + +// Installed before any wire activity happens. +let mut events = prepared.subscribe(); +tokio::spawn(async move { + while let Ok(event) = events.recv().await { + println!("{}", event.event_type); + } +}); + +let session = prepared.start().await?; +``` + +`prepare_*` is synchronous and inert — it validates the buffer capacity, allocates a local channel and cancellation token, and touches neither the router nor the transport until `start()` is first polled. `start(self)` consumes the handle and `PreparedSession` is deliberately not `Clone`, so a prepared session can never spawn two event loops. Dropping an unstarted handle leaves no state and closes its subscriptions; dropping the `start()` future cancels the startup, unregisters the session, and lets a same-ID retry succeed. Cleanup removes only the exact registration that startup owned, so a retry started while an abandoned attempt is still unwinding is never evicted by it. + +The buffer is finite — `session::DEFAULT_EVENT_BUFFER_CAPACITY` (512) unless `event_buffer_capacity` overrides it, and `Some(0)` is rejected as `ErrorKind::InvalidConfig` rather than clamped. Subscribers that fall behind observe `RecvErrorKind::Lagged` with the skipped count instead of applying backpressure, so a consumer that needs a lossless view of a large startup burst must configure enough capacity or drain concurrently with `start()`. + +For cloud sessions where the server assigns the session ID, notifications can't be routed until the create response arrives; the guarantee is that *routed* events are never dropped for lack of a receiver. Pin `session_id` for full pre-response coverage. + +`create_session` / `resume_session` are unchanged wrappers over `prepare_*(...)?.start()`, with identical RPC sequences and error kinds. + ### Infinite Sessions Enable the SDK's session-store integration so conversations persist across CLI restarts and grow beyond the model's context window via automatic compaction: @@ -920,6 +950,12 @@ none of them are scheduled for removal. arg vectors for "prepend before subcommand" vs "append after the built-in flags", giving precise control over CLI invocation order without string-splicing. +- **`Client::prepare_session` / `prepare_resume_session`** — return an inert + `PreparedSession` whose `subscribe()` installs an event receiver before any + protocol activity, so startup events (including ephemeral `session.idle`) + aren't dropped. Other SDKs register callbacks on a config object instead, + which sidesteps the problem in a way Rust's broadcast-based `subscribe()` + cannot. ## Layout @@ -927,7 +963,7 @@ none of them are scheduled for removal. | ----------------- | -------------------------------------------------------------------------------------------------------------------------- | | `lib.rs` | `Client`, `ClientOptions`, `CliProgram`, `Transport`, `Error` | | `extension_launch_provider.rs` | Connection-global `ExtensionLaunchProvider` trait and launch profile DTOs | -| `session.rs` | `Session` struct, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session` | +| `session.rs` | `Session` struct, `PreparedSession`, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session`/`prepare_session`/`prepare_resume_session` | | `subscription.rs` | `EventSubscription` / `LifecycleSubscription` (`Stream`-able observer handles for `subscribe()` / `subscribe_lifecycle()`) | | `handler.rs` | `PermissionHandler`, `ElicitationHandler`, `UserInputHandler`, `ExitPlanModeHandler`, `AutoModeSwitchHandler` traits; `ApproveAllHandler`, `DenyAllHandler` | | `hooks.rs` | `SessionHooks` trait, `HookEvent`/`HookOutput` enums, typed hook inputs/outputs | diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 0a3886a82e..01d5dec5a4 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2324,15 +2324,18 @@ impl Client { /// Register a session to receive filtered events and requests. /// - /// Returns per-session channels for notifications and requests, routed - /// by `sessionId`. Starts the internal router on first call. - /// - /// When done, call [`unregister_session`](Self::unregister_session) to - /// clean up (typically on session destroy). + /// Returns the per-session channels plus a + /// [`RegistrationToken`](crate::router::RegistrationToken) identifying + /// *this* registration. Registering an ID that is already registered + /// replaces the previous registration. + /// + /// When done, call + /// [`unregister_session_owned`](Self::unregister_session_owned) with + /// that token to clean up (typically on session destroy). pub(crate) fn register_session( &self, session_id: &SessionId, - ) -> crate::router::SessionChannels { + ) -> crate::router::SessionRegistration { self.inner.router.ensure_started( &self.inner.notification_tx, &self.inner.request_rx, @@ -2344,9 +2347,30 @@ impl Client { self.inner.router.register(session_id) } - /// Unregister a session, dropping its per-session channels. - pub(crate) fn unregister_session(&self, session_id: &SessionId) { - self.inner.router.unregister(session_id); + /// Unregister a session only if `token` still identifies the live + /// registration. + /// + /// Session IDs can be reused: a caller may retry a cancelled startup + /// with the same pinned ID while the previous owner is still being torn + /// down. Compare-and-remove keeps a stale owner from unregistering the + /// live session that replaced it. + pub(crate) fn unregister_session_owned( + &self, + session_id: &SessionId, + token: crate::router::RegistrationToken, + ) { + self.inner.router.unregister_owned(session_id, token); + } + + /// Snapshot the session IDs currently registered on the router. + /// + /// Crate-internal so in-crate unit tests can assert registration + /// lifecycle without depending on the `test-support` feature, which + /// only gates the equivalent *public* test helper. Compiled only for + /// those two configurations — a default-feature build has no caller. + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn registered_session_ids(&self) -> Vec { + self.inner.router.session_ids() } pub(crate) fn register_github_token_provider( @@ -2596,6 +2620,24 @@ impl Client { ); } + #[cfg(feature = "test-support")] + #[doc(hidden)] + /// Snapshot the session IDs currently registered on this client's + /// notification router. This is test-harness plumbing, not part of the + /// supported SDK API. + pub fn registered_session_ids_for_test(&self) -> Vec { + self.registered_session_ids() + } + + #[cfg(feature = "test-support")] + #[doc(hidden)] + /// Count the sessions currently registered on this client's notification + /// router. Deliberately never materialises the session IDs themselves so + /// they cannot leak into test diagnostics. + pub fn registered_session_count_for_test(&self) -> usize { + self.inner.router.session_count() + } + #[cfg(feature = "test-support")] #[doc(hidden)] /// Disconnect and delete every session owned by this test client's isolated diff --git a/rust/src/router.rs b/rust/src/router.rs index 4815d0e1c5..2b09713723 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use parking_lot::Mutex; use tokio::sync::{broadcast, mpsc}; @@ -8,6 +9,24 @@ use tracing::warn; use crate::jsonrpc::{JsonRpcNotification, JsonRpcRequest}; use crate::types::{SessionEventNotification, SessionId}; +/// Identity of one specific registration of a session ID. +/// +/// Session IDs are not unique over time: a caller can retry a cancelled +/// startup with the same pinned ID, and the retry replaces the previous +/// registration. Removal is therefore compare-and-remove against this +/// token, so a stale owner (an aborted startup future or a superseded +/// [`Session`](crate::session::Session)) can never unregister the live +/// registration that replaced it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct RegistrationToken(u64); + +/// Per-session channels plus the identity of the registration that owns +/// them. Returned by [`SessionRouter::register`]. +pub(crate) struct SessionRegistration { + pub(crate) channels: SessionChannels, + pub(crate) token: RegistrationToken, +} + /// Per-session channels created by the router during session registration. pub(crate) struct SessionChannels { /// Filtered `session.event` notifications for this session. @@ -19,6 +38,7 @@ pub(crate) struct SessionChannels { struct SessionSenders { notifications: mpsc::UnboundedSender, requests: mpsc::UnboundedSender, + token: RegistrationToken, } /// Routes notifications and requests by sessionId to per-session channels. @@ -26,6 +46,7 @@ struct SessionSenders { /// Internal to the SDK — consumers interact via `Client::register_session()`. pub(crate) struct SessionRouter { sessions: Arc>>, + next_token: AtomicU64, started: Mutex, } @@ -33,32 +54,69 @@ impl SessionRouter { pub(crate) fn new() -> Self { Self { sessions: Arc::new(Mutex::new(HashMap::new())), + next_token: AtomicU64::new(0), started: Mutex::new(false), } } /// Register a session to receive filtered events and requests. - pub(crate) fn register(&self, session_id: &SessionId) -> SessionChannels { + /// + /// Replaces any existing registration for the same ID and returns a + /// fresh [`RegistrationToken`] identifying this registration. + pub(crate) fn register(&self, session_id: &SessionId) -> SessionRegistration { let (notif_tx, notif_rx) = mpsc::unbounded_channel(); let (req_tx, req_rx) = mpsc::unbounded_channel(); + let token = RegistrationToken(self.next_token.fetch_add(1, Ordering::Relaxed)); self.sessions.lock().insert( session_id.clone(), SessionSenders { notifications: notif_tx, requests: req_tx, + token, }, ); - SessionChannels { - notifications: notif_rx, - requests: req_rx, + SessionRegistration { + channels: SessionChannels { + notifications: notif_rx, + requests: req_rx, + }, + token, } } /// Unregister a session, dropping its channels. + /// + /// Unconditional: removes whichever registration currently holds the + /// ID. Only for client-wide teardown, where every session is going away + /// regardless of owner. Owners of a specific registration must use + /// [`unregister_owned`](Self::unregister_owned). pub(crate) fn unregister(&self, session_id: &SessionId) { self.sessions.lock().remove(session_id.as_str()); } + /// Unregister a session only if it is still the registration identified + /// by `token`. + /// + /// Returns `true` when the entry was removed. A `false` result means + /// the registration had already been replaced by a newer one, which the + /// caller does not own and must leave alone. + pub(crate) fn unregister_owned( + &self, + session_id: &SessionId, + token: RegistrationToken, + ) -> bool { + let mut sessions = self.sessions.lock(); + if sessions + .get(session_id.as_str()) + .is_some_and(|senders| senders.token == token) + { + sessions.remove(session_id.as_str()); + true + } else { + false + } + } + /// Snapshot every currently-registered session ID. /// /// Used by [`Client::stop`](crate::Client::stop) to iterate active @@ -68,6 +126,12 @@ impl SessionRouter { self.sessions.lock().keys().cloned().collect() } + /// Count the currently-registered sessions without exposing their IDs. + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn session_count(&self) -> usize { + self.sessions.lock().len() + } + /// Drop all registered session channels. /// /// Used by [`Client::force_stop`](crate::Client::force_stop) to release @@ -238,3 +302,37 @@ impl SessionRouter { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn session_id() -> SessionId { + SessionId::new("router-ownership") + } + + #[test] + fn each_registration_gets_a_distinct_token() { + let router = SessionRouter::new(); + let first = router.register(&session_id()); + let second = router.register(&session_id()); + assert_ne!(first.token, second.token); + } + + #[test] + fn unregister_owned_removes_only_the_matching_registration() { + let router = SessionRouter::new(); + let stale = router.register(&session_id()); + let live = router.register(&session_id()); + + // The stale owner must not evict the registration that replaced it. + assert!(!router.unregister_owned(&session_id(), stale.token)); + assert_eq!(router.session_ids(), vec![session_id()]); + + assert!(router.unregister_owned(&session_id(), live.token)); + assert!(router.session_ids().is_empty()); + + // Removing twice is a no-op rather than evicting a future tenant. + assert!(!router.unregister_owned(&session_id(), live.token)); + } +} diff --git a/rust/src/session.rs b/rust/src/session.rs index 7cf207d104..f935beca99 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -50,6 +50,31 @@ use crate::{ /// `overrides_built_in_tool` set to `true`. const TOOL_SEARCH_TOOL_NAME: &str = "tool_search_tool"; +/// Default capacity of the per-session event broadcast buffer backing +/// [`Session::subscribe`] and [`PreparedSession::subscribe`]. +/// +/// Override per session with +/// [`SessionConfig::event_buffer_capacity`](crate::types::SessionConfig::event_buffer_capacity) +/// or +/// [`ResumeSessionConfig::event_buffer_capacity`](crate::types::ResumeSessionConfig::event_buffer_capacity). +pub const DEFAULT_EVENT_BUFFER_CAPACITY: usize = 512; + +/// Validate a caller-supplied event buffer capacity and resolve the default. +/// +/// Zero is rejected rather than clamped: a zero-capacity broadcast channel +/// cannot exist, and silently substituting a different capacity would hide a +/// caller bug. +fn resolve_event_buffer_capacity(capacity: Option) -> Result { + match capacity { + Some(0) => Err(Error::with_message( + ErrorKind::InvalidConfig, + "event_buffer_capacity must be greater than zero", + )), + Some(capacity) => Ok(capacity), + None => Ok(DEFAULT_EVENT_BUFFER_CAPACITY), + } +} + /// Bundle of the per-session callbacks the SDK dispatches to. Built from a /// [`SessionConfig`] / [`ResumeSessionConfig`] at /// [`Client::create_session`] / [`Client::resume_session`] time. Each @@ -107,25 +132,82 @@ impl Drop for WaiterGuard { struct PendingSessionRegistration { client: Client, - session_id: SessionId, + session_id: PendingSessionId, shutdown: CancellationToken, disarmed: bool, } +/// Which session ID a [`PendingSessionRegistration`] should unregister on +/// cleanup. +/// +/// `session.create` for cloud sessions without a caller-pinned ID does not +/// know the ID until the response arrives, at which point the inline +/// response callback registers it and stashes it. The guard therefore reads +/// the stash at cleanup time instead of capturing an ID up front. +enum PendingSessionId { + /// The ID was known before the RPC was issued (resume, and create with a + /// client- or caller-supplied ID). + Known(SessionId, crate::router::RegistrationToken), + /// Server-assigned ID, populated by the `session.create` inline response + /// callback. `None` in the stash means nothing was ever registered. + Deferred(Arc>>), +} + impl PendingSessionRegistration { - fn new(client: Client, session_id: SessionId, shutdown: CancellationToken) -> Self { + fn new( + client: Client, + session_id: SessionId, + token: crate::router::RegistrationToken, + shutdown: CancellationToken, + ) -> Self { Self { client, - session_id, + session_id: PendingSessionId::Known(session_id, token), shutdown, disarmed: false, } } + /// Guard for a registration whose session ID is assigned by the server. + fn deferred( + client: Client, + stash: Arc>>, + shutdown: CancellationToken, + ) -> Self { + Self { + client, + session_id: PendingSessionId::Deferred(stash), + shutdown, + disarmed: false, + } + } + + fn registered_id(&self) -> Option { + match &self.session_id { + PendingSessionId::Known(id, _) => Some(id.clone()), + PendingSessionId::Deferred(stash) => stash.lock().as_ref().map(|(id, _)| id.clone()), + } + } + + /// Re-target the guard at a now-known session ID. Used by + /// `session.create` once the response has been parsed and the stash has + /// been drained into the event loop. + fn resolve_to(&mut self, session_id: SessionId, token: crate::router::RegistrationToken) { + self.session_id = PendingSessionId::Known(session_id, token); + } + async fn cleanup(mut self, event_loop: JoinHandle<()>) { self.shutdown.cancel(); let _ = event_loop.await; - self.client.unregister_session(&self.session_id); + if let Some(id) = self.registered_id() { + if let PendingSessionId::Known(_, token) = self.session_id { + self.client.unregister_session_owned(&id, token); + } else if let PendingSessionId::Deferred(stash) = &self.session_id + && let Some((id, registration)) = stash.lock().as_ref() + { + self.client.unregister_session_owned(id, registration.token); + } + } self.disarmed = true; } @@ -138,7 +220,15 @@ impl Drop for PendingSessionRegistration { fn drop(&mut self) { if !self.disarmed { self.shutdown.cancel(); - self.client.unregister_session(&self.session_id); + if let Some(id) = self.registered_id() { + if let PendingSessionId::Known(_, token) = self.session_id { + self.client.unregister_session_owned(&id, token); + } else if let PendingSessionId::Deferred(stash) = &self.session_id + && let Some((id, registration)) = stash.lock().as_ref() + { + self.client.unregister_session_owned(id, registration.token); + } + } } } } @@ -195,6 +285,8 @@ pub struct Session { event_tx: tokio::sync::broadcast::Sender, github_token_registration: ParkingLotMutex>, + /// Identity of this session's router registration. + registration_token: crate::router::RegistrationToken, } impl Session { @@ -653,8 +745,9 @@ impl Session { ) .await?; self.stop_event_loop().await; - self.client.unregister_session(&self.id); self.github_token_registration.lock().take(); + self.client + .unregister_session_owned(&self.id, self.registration_token); Ok(()) } @@ -731,8 +824,9 @@ impl Drop for Session { // tokio runtime when it next polls; we intentionally don't await // it here because Drop is sync. self.shutdown.cancel(); - self.client.unregister_session(&self.id); self.github_token_registration.lock().take(); + self.client + .unregister_session_owned(&self.id, self.registration_token); } } @@ -874,6 +968,102 @@ impl<'a> SessionUi<'a> { } impl Client { + /// Prepare a new session without touching the transport. + /// + /// Returns a [`PreparedSession`] that owns the session's event broadcast + /// channel, so callers can install an + /// [`EventSubscription`](crate::subscription::EventSubscription) via + /// [`PreparedSession::subscribe`] *before* any protocol activity starts. + /// Call [`PreparedSession::start`] to actually create the session. + /// + /// This is the loss-free entry point for consumers that must observe + /// every *routed* event a session emits, including events the CLI emits + /// while `session.create` is still in flight and ephemeral events (such + /// as `session.idle`) that cannot be recovered from + /// [`Session::get_messages`]. [`create_session`](Self::create_session) + /// is a thin wrapper over `prepare_session(...)?.start()` and cannot + /// offer the same guarantee, because the subscription can only be + /// installed after the returned `Session` exists. + /// + /// Routing requires a known session ID. When the server assigns the ID, + /// the SDK cannot register the session on its notification router until + /// the `session.create` response arrives, so notifications emitted + /// before that point are not routable and stay unobservable. Pin + /// [`SessionConfig::session_id`](crate::types::SessionConfig::session_id) + /// for complete pre-response coverage — see the "Server-assigned session + /// IDs" section on [`PreparedSession`]. + /// + /// # Inertness + /// + /// `prepare_session` performs no router registration, spawns no task, + /// and writes nothing to the wire. It only validates + /// [`event_buffer_capacity`](SessionConfig::event_buffer_capacity), + /// allocates a local broadcast channel and cancellation token, and + /// stores the config. Dropping the returned handle without starting it + /// leaves no client-side or server-side state behind and closes every + /// subscription taken from it. + /// + /// # Errors + /// + /// Returns [`ErrorKind::InvalidConfig`] if + /// [`event_buffer_capacity`](SessionConfig::event_buffer_capacity) is + /// `Some(0)`. All other configuration and protocol errors surface from + /// [`PreparedSession::start`], with the same + /// [`ErrorKind`]s [`create_session`](Self::create_session) has always + /// returned. + /// + /// # Example + /// + /// ```no_run + /// # use github_copilot_sdk::{Client, SessionConfig}; + /// # async fn example(client: Client) -> Result<(), github_copilot_sdk::Error> { + /// let prepared = client.prepare_session(SessionConfig::default())?; + /// let mut events = prepared.subscribe(); + /// let drain = tokio::spawn(async move { + /// while let Ok(event) = events.recv().await { + /// println!("{}", event.event_type); + /// } + /// }); + /// let session = prepared.start().await?; + /// # let _ = (session, drain); + /// # Ok(()) + /// # } + /// ``` + pub fn prepare_session(&self, config: SessionConfig) -> Result { + let capacity = resolve_event_buffer_capacity(config.event_buffer_capacity)?; + Ok(PreparedSession::new( + self.clone(), + PreparedKind::Create(Box::new(config)), + capacity, + )) + } + + /// Prepare a session resume without touching the transport. + /// + /// The resume counterpart of [`prepare_session`](Self::prepare_session); + /// see that method for the inertness guarantee, error semantics, and + /// rationale. Particularly relevant on resume with + /// [`continue_pending_work`](ResumeSessionConfig::continue_pending_work), + /// where the runtime can start emitting events (and reach + /// `session.idle`) while `session.resume` is still in flight. + /// + /// # Errors + /// + /// Returns [`ErrorKind::InvalidConfig`] if + /// [`event_buffer_capacity`](ResumeSessionConfig::event_buffer_capacity) + /// is `Some(0)`. + pub fn prepare_resume_session( + &self, + config: ResumeSessionConfig, + ) -> Result { + let capacity = resolve_event_buffer_capacity(config.event_buffer_capacity)?; + Ok(PreparedSession::new( + self.clone(), + PreparedKind::Resume(Box::new(config)), + capacity, + )) + } + /// Create a new session on the CLI. /// /// Sends `session.create`, registers the session on the router, @@ -895,7 +1085,48 @@ impl Client { /// Each per-event handler is independently optional. If a handler is /// not installed, the SDK signals the runtime not to emit the matching /// broadcast (and silently skips dispatch if one arrives anyway). - pub async fn create_session(&self, mut config: SessionConfig) -> Result { + /// + /// # Event delivery + /// + /// Equivalent to `prepare_session(config)?.start().await`. Because the + /// first subscription can only be taken from the returned [`Session`], + /// events the runtime emits before this call returns are broadcast with + /// no receiver installed and are therefore not delivered to + /// [`Session::subscribe`]. Use + /// [`prepare_session`](Self::prepare_session) when startup events + /// matter. + pub async fn create_session(&self, config: SessionConfig) -> Result { + self.prepare_session(config)?.start().await + } + + /// Resume an existing session on the CLI. + /// + /// Sends `session.resume` and `session.skills.reload`, registers the + /// session on the router, and spawns the event loop. + /// + /// All callbacks (event handler, hooks, transform) are configured + /// via [`ResumeSessionConfig`] using its `with_*` builder methods. + /// + /// See [`Self::create_session`] for the defaults applied when callback + /// fields are unset. + /// + /// # Event delivery + /// + /// Equivalent to `prepare_resume_session(config)?.start().await`, and + /// carries the same startup-event caveat documented on + /// [`create_session`](Self::create_session). Use + /// [`prepare_resume_session`](Self::prepare_resume_session) when + /// startup events matter. + pub async fn resume_session(&self, config: ResumeSessionConfig) -> Result { + self.prepare_resume_session(config)?.start().await + } + + async fn start_prepared_create( + &self, + mut config: SessionConfig, + event_tx: tokio::sync::broadcast::Sender, + shutdown: CancellationToken, + ) -> Result { let total_start = Instant::now(); // For cloud sessions, let the CLI/server assign the session id and // register the session lazily once the response arrives. For non-cloud @@ -1036,8 +1267,6 @@ impl Client { let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default())); let idle_waiter = Arc::new(ParkingLotMutex::new(None)); let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new())); - let shutdown = CancellationToken::new(); - let (event_tx, _) = tokio::sync::broadcast::channel(512); // For cloud sessions (use_server_generated_id), defer session // registration to the inline callback so the read task registers @@ -1045,7 +1274,7 @@ impl Client { // For non-cloud sessions, register up-front so the CLI can issue // session-scoped requests during session.create processing. let inline_stash: Arc< - ParkingLotMutex>, + ParkingLotMutex>, > = Arc::new(ParkingLotMutex::new(None)); let inline_callback: Option = if let Some(ref sid) = @@ -1073,45 +1302,55 @@ impl Client { }) .into()); } - let channels = client.register_session(&parsed.session_id); - *stash.lock() = Some((parsed.session_id, channels)); + // Register and stash under a single stash-lock hold. The + // cancellation guard identifies the session to unregister by + // peeking this stash, so registering outside the lock would + // leave a window where a concurrent guard drop (caller + // cancellation) sees `None` and leaks the registration. + // `register_session` takes the router lock, never the stash + // lock, so there is no lock-order inversion here. + let mut stashed = stash.lock(); + let registration = client.register_session(&parsed.session_id); + *stashed = Some((parsed.session_id, registration)); Ok(()) })) }; - let rpc_start = Instant::now(); - let result = match self - .call_with_inline_callback("session.create", Some(params), inline_callback) - .await - { - Ok(result) => result, - Err(error) => { - if let Some((id, _channels)) = inline_stash.lock().take() { - self.unregister_session(&id); - } - return Err(error); + // Armed for the whole startup sequence: any early return, and any + // drop of this future (caller cancellation), cancels the session + // token and unregisters whatever was registered on the router. For + // the cloud path the ID is only known once the inline callback has + // run, so the guard reads the stash at cleanup time. + let mut pending_registration = match local_session_id { + Some(ref sid) => { + let token = inline_stash + .lock() + .as_ref() + .expect("session registration must exist") + .1 + .token; + PendingSessionRegistration::new(self.clone(), sid.clone(), token, shutdown.clone()) } + None => PendingSessionRegistration::deferred( + self.clone(), + inline_stash.clone(), + shutdown.clone(), + ), }; + + let rpc_start = Instant::now(); + let result = self + .call_with_inline_callback("session.create", Some(params), inline_callback) + .await?; tracing::debug!( elapsed_ms = rpc_start.elapsed().as_millis(), "Client::create_session session creation request completed successfully" ); - let create_result: CreateSessionResult = match serde_json::from_value(result) { - Ok(result) => result, - Err(error) => { - if let Some((id, _channels)) = inline_stash.lock().take() { - self.unregister_session(&id); - } - return Err(error.into()); - } - }; + let create_result: CreateSessionResult = serde_json::from_value(result)?; if let Some(ref requested) = local_session_id && create_result.session_id != *requested { - if let Some((id, _channels)) = inline_stash.lock().take() { - self.unregister_session(&id); - } return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch { requested: requested.clone(), returned: create_result.session_id.clone(), @@ -1119,10 +1358,13 @@ impl Client { .into()); } - let (session_id, channels) = inline_stash + let (session_id, registration) = inline_stash .lock() .take() .expect("session registration must have populated stash on success"); + let channels = registration.channels; + let registration_token = registration.token; + pending_registration.resolve_to(session_id.clone(), registration_token); let event_loop = spawn_event_loop( session_id.clone(), self.clone(), @@ -1149,8 +1391,11 @@ 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?; + if has_mcp_auth_handler + && let Err(error) = register_mcp_auth_interest(self, &session_id).await + { + pending_registration.cleanup(event_loop).await; + return Err(error); } tracing::debug!( @@ -1158,6 +1403,7 @@ impl Client { session_id = %session_id, "Client::create_session complete" ); + pending_registration.disarm(); let session = Session { id: session_id, cwd: self.cwd().clone(), @@ -1171,6 +1417,7 @@ impl Client { open_canvases, event_tx, github_token_registration: ParkingLotMutex::new(github_token_registration), + registration_token, }; apply_mode_post_create_patch( &session, @@ -1200,7 +1447,12 @@ impl Client { /// /// See [`Self::create_session`] for the defaults applied when callback /// fields are unset. - pub async fn resume_session(&self, mut config: ResumeSessionConfig) -> Result { + async fn start_prepared_resume( + &self, + mut config: ResumeSessionConfig, + event_tx: tokio::sync::broadcast::Sender, + shutdown: CancellationToken, + ) -> Result { let total_start = Instant::now(); let session_id = config.session_id.clone(); if config.hooks_handler.is_some() && config.hooks.is_none() { @@ -1322,11 +1574,11 @@ impl Client { let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default())); let setup_start = Instant::now(); - let channels = self.register_session(&session_id); + let registration = self.register_session(&session_id); + let registration_token = registration.token; + let channels = registration.channels; let idle_waiter = Arc::new(ParkingLotMutex::new(None)); let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new())); - let shutdown = CancellationToken::new(); - let (event_tx, _) = tokio::sync::broadcast::channel(512); let event_loop = spawn_event_loop( session_id.clone(), self.clone(), @@ -1344,8 +1596,12 @@ impl Client { event_tx.clone(), shutdown.clone(), ); - let mut registration = - PendingSessionRegistration::new(self.clone(), session_id.clone(), shutdown.clone()); + let mut registration = PendingSessionRegistration::new( + self.clone(), + session_id.clone(), + registration_token, + shutdown.clone(), + ); tracing::debug!( elapsed_ms = setup_start.elapsed().as_millis(), session_id = %session_id, @@ -1388,10 +1644,12 @@ impl Client { }) .into()); } - if has_mcp_auth_handler { - register_mcp_auth_interest(self, &session_id).await?; + if has_mcp_auth_handler + && let Err(error) = register_mcp_auth_interest(self, &session_id).await + { + registration.cleanup(event_loop).await; + return Err(error); } - // Reload skills after resume (best-effort). let skills_reload_start = Instant::now(); if let Err(e) = self @@ -1446,6 +1704,7 @@ impl Client { open_canvases, event_tx, github_token_registration: ParkingLotMutex::new(github_token_registration), + registration_token, }; apply_mode_post_create_patch( &session, @@ -1466,6 +1725,149 @@ impl Client { } } +/// A session that has been configured but not yet created on the CLI. +/// +/// Returned by [`Client::prepare_session`] and +/// [`Client::prepare_resume_session`]. Its purpose is to make the session's +/// event stream observable *before* any protocol activity starts: +/// [`subscribe`](Self::subscribe) installs a receiver on the same broadcast +/// channel the eventual [`Session`] uses, so events the runtime emits while +/// `session.create` / `session.resume` is still in flight are delivered +/// rather than dropped for lack of a receiver. +/// +/// # Lifecycle +/// +/// A prepared handle is inert. It holds only a broadcast sender, a +/// cancellation token, the client handle, and the config — it performs no +/// router registration, spawns no task, and writes nothing to the wire +/// until [`start`](Self::start) is first polled. +/// +/// * Dropping it without starting leaves no client-side or server-side +/// state, and closes every subscription taken from it. +/// * Dropping the [`start`](Self::start) future mid-flight cancels the +/// session token, unregisters the session from the router if it was +/// registered, and closes early subscriptions. A retry with the same +/// session ID succeeds. Cleanup of already-spawned tasks is signalled, +/// not awaited: `Drop` is synchronous and cannot await, so the event loop +/// terminates promptly but not synchronously. +/// * A startup error from [`start`](Self::start) performs the same cleanup +/// and preserves the [`ErrorKind`] the equivalent +/// [`Client::create_session`] / [`Client::resume_session`] call has always +/// returned. +/// +/// [`start`](Self::start) consumes `self` and the type is deliberately not +/// [`Clone`], so a prepared session can be started at most once and can +/// never produce two event loops. +/// +/// # Buffering +/// +/// The broadcast buffer is finite — +/// [`DEFAULT_EVENT_BUFFER_CAPACITY`] unless +/// [`SessionConfig::event_buffer_capacity`] / +/// [`ResumeSessionConfig::event_buffer_capacity`] overrides it. Subscribers +/// that fall behind observe +/// [`Lagged`](crate::subscription::Lagged) instead of applying backpressure +/// to the event loop. Consumers that need a lossless view of a large +/// startup burst must either configure a capacity that covers it or drain +/// the subscription concurrently with [`start`](Self::start). +/// +/// # Server-assigned session IDs +/// +/// For cloud sessions without a caller-supplied session ID, the CLI assigns +/// the ID and the SDK can only register the session on its notification +/// router once the `session.create` response arrives. Notifications the +/// server emits before that point are not routable to any session and are +/// therefore not observable. The guarantee this type provides is narrower +/// and precise: **routed** events are never dropped for lack of an +/// installed receiver. Pin +/// [`SessionConfig::session_id`](crate::types::SessionConfig::session_id) +/// to get registration before the RPC and full pre-response coverage. +#[must_use = "a PreparedSession does nothing until started"] +pub struct PreparedSession { + client: Client, + kind: PreparedKind, + event_tx: tokio::sync::broadcast::Sender, + shutdown: CancellationToken, +} + +/// Which startup path a [`PreparedSession`] runs when started. Boxed +/// because the two config types are large and differently sized. +enum PreparedKind { + Create(Box), + Resume(Box), +} + +impl PreparedSession { + fn new(client: Client, kind: PreparedKind, event_buffer_capacity: usize) -> Self { + let (event_tx, _) = tokio::sync::broadcast::channel(event_buffer_capacity); + Self { + client, + kind, + event_tx, + shutdown: CancellationToken::new(), + } + } + + /// Subscribe to this session's events before it starts. + /// + /// The returned [`EventSubscription`](crate::subscription::EventSubscription) + /// is backed by the same broadcast channel + /// [`Session::subscribe`] returns after [`start`](Self::start) + /// succeeds, so a subscription taken here observes the full event + /// stream from the session's first routed event onward — including + /// ephemeral events such as `session.idle` that + /// [`Session::get_messages`] cannot recover. + /// + /// May be called any number of times, and each subscriber receives its + /// own copy of the stream — subject to the buffering contract above. A + /// subscriber that falls further behind than the configured capacity + /// observes [`Lagged`](crate::subscription::Lagged) and skips the + /// events it missed, rather than stalling the session's event loop. + /// Subscriptions taken here close if the prepared session is dropped + /// without starting, or if startup fails. + pub fn subscribe(&self) -> crate::subscription::EventSubscription { + crate::subscription::EventSubscription::new(self.event_tx.subscribe()) + } + + /// Create or resume the session on the CLI. + /// + /// This is where all protocol activity happens: config validation, + /// router registration, the `session.create` / `session.resume` RPC, + /// and the event loop spawn. Nothing observable occurs until this + /// future is first polled. + /// + /// # Errors + /// + /// Returns the same errors as [`Client::create_session`] / + /// [`Client::resume_session`] — including + /// [`ErrorKind::InvalidConfig`] for invalid configs, transport and RPC + /// failures, and + /// [`SessionIdMismatch`](crate::SessionErrorKind::SessionIdMismatch) + /// when the CLI returns a different session ID than the one requested. + /// Every error path unregisters the session and closes subscriptions + /// taken from this handle. + pub async fn start(self) -> Result { + let Self { + client, + kind, + event_tx, + shutdown, + } = self; + match kind { + PreparedKind::Create(config) => { + client + .start_prepared_create(*config, event_tx, shutdown) + .await + } + PreparedKind::Resume(config) => { + client + .start_prepared_resume(*config, event_tx, shutdown) + .await + } + } + } +} + type CommandHandlerMap = HashMap>; async fn apply_mode_post_create_patch( diff --git a/rust/src/types.rs b/rust/src/types.rs index 32b504e336..a99f00a19f 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -2271,6 +2271,23 @@ pub struct SessionConfig { /// `session.options.update` after create/resume. Defaults to `false` in /// [`crate::ClientMode::Empty`] when unset. pub manage_schedule_enabled: Option, + /// Capacity of the per-session broadcast buffer backing + /// [`Session::subscribe`](crate::session::Session::subscribe) and + /// [`PreparedSession::subscribe`](crate::session::PreparedSession::subscribe). + /// + /// Runtime-only — never sent on the wire. Defaults to + /// [`DEFAULT_EVENT_BUFFER_CAPACITY`](crate::session::DEFAULT_EVENT_BUFFER_CAPACITY) + /// when unset. Must be non-zero; + /// `Some(0)` is rejected with + /// [`ErrorKind::InvalidConfig`](crate::ErrorKind::InvalidConfig) by + /// [`Client::prepare_session`](crate::Client::prepare_session). + /// + /// The buffer is finite: subscribers that fall behind observe + /// [`Lagged`](crate::subscription::Lagged) rather than applying + /// backpressure to the event loop. Raise this when a consumer needs a + /// lossless view of a large startup burst without draining + /// concurrently. + pub event_buffer_capacity: Option, } impl std::fmt::Debug for SessionConfig { @@ -2406,6 +2423,7 @@ impl std::fmt::Debug for SessionConfig { "system_message_transform", &self.system_message_transform.as_ref().map(|_| ""), ) + .field("event_buffer_capacity", &self.event_buffer_capacity) .finish() } } @@ -2502,6 +2520,7 @@ impl Default for SessionConfig { enable_experimental_mode: None, coauthor_enabled: None, manage_schedule_enabled: None, + event_buffer_capacity: None, } } } @@ -3305,6 +3324,18 @@ impl SessionConfig { self } + /// Set [`Self::event_buffer_capacity`]. + /// + /// A capacity of `0` is rejected with + /// [`ErrorKind::InvalidConfig`](crate::ErrorKind::InvalidConfig) by + /// [`Client::prepare_session`](crate::Client::prepare_session) and + /// [`Client::create_session`](crate::Client::create_session); the value + /// is never clamped. + pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self { + self.event_buffer_capacity = Some(capacity); + self + } + /// Inject ExP assignment ("flight") data for this session, in the same /// JSON shape the Copilot CLI fetches from the experimentation service /// (`CopilotExpAssignmentResponse`). The runtime feeds it into the same @@ -3607,6 +3638,8 @@ pub struct ResumeSessionConfig { pub coauthor_enabled: Option, /// See [`SessionConfig::manage_schedule_enabled`]. pub manage_schedule_enabled: Option, + /// See [`SessionConfig::event_buffer_capacity`]. + pub event_buffer_capacity: Option, } impl std::fmt::Debug for ResumeSessionConfig { @@ -3740,6 +3773,7 @@ impl std::fmt::Debug for ResumeSessionConfig { ) .field("suppress_resume_event", &self.suppress_resume_event) .field("continue_pending_work", &self.continue_pending_work) + .field("event_buffer_capacity", &self.event_buffer_capacity) .finish() } } @@ -3991,6 +4025,7 @@ impl ResumeSessionConfig { enable_experimental_mode: None, coauthor_enabled: None, manage_schedule_enabled: None, + event_buffer_capacity: None, } } @@ -4602,6 +4637,18 @@ impl ResumeSessionConfig { self } + /// Set [`Self::event_buffer_capacity`]. + /// + /// A capacity of `0` is rejected with + /// [`ErrorKind::InvalidConfig`](crate::ErrorKind::InvalidConfig) by + /// [`Client::prepare_resume_session`](crate::Client::prepare_resume_session) + /// and [`Client::resume_session`](crate::Client::resume_session); the + /// value is never clamped. + pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self { + self.event_buffer_capacity = Some(capacity); + self + } + /// Inject ExP assignment ("flight") data on resume. See /// [`SessionConfig::with_exp_assignments`]. Re-supply the assignments on /// resume so the runtime re-applies them after a CLI process restart. diff --git a/rust/tests/prepared_session_test.rs b/rust/tests/prepared_session_test.rs new file mode 100644 index 0000000000..3fefccabb6 --- /dev/null +++ b/rust/tests/prepared_session_test.rs @@ -0,0 +1,1242 @@ +//! Early event subscription via `Client::prepare_session` / +//! `Client::prepare_resume_session`. +//! +//! Every test drives the SDK over an in-memory duplex transport and a +//! hand-rolled JSON-RPC peer, so event ordering is deterministic. Timeouts +//! are failure backstops only — no test sleeps to "let things settle". + +#![allow(clippy::unwrap_used)] + +use std::marker::PhantomData; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::handler::{McpAuthHandler, McpAuthRequest, McpAuthResult}; +use github_copilot_sdk::session::PreparedSession; +use github_copilot_sdk::subscription::{EventSubscription, RecvErrorKind}; +use github_copilot_sdk::types::{ + CloudSessionOptions, CloudSessionRepository, RequestId, ResumeSessionConfig, SessionConfig, + SessionId, +}; +use github_copilot_sdk::{Client, ErrorKind, SessionErrorKind}; +use serde_json::{Value, json}; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, duplex}; +use tokio::time::timeout; + +/// Failure backstop for operations that must complete promptly. +const TIMEOUT: Duration = Duration::from_secs(5); +/// Backstop for asserting that something does *not* happen. +const QUIET: Duration = Duration::from_millis(150); +/// Size of the pre-response event burst. Mirrors the copilot-host startup +/// burst that motivated the API. +const BURST: usize = 600; + +// --------------------------------------------------------------------------- +// Transport harness +// --------------------------------------------------------------------------- + +async fn write_framed(writer: &mut (impl AsyncWrite + Unpin), body: &[u8]) { + let header = format!("Content-Length: {}\r\n\r\n", body.len()); + writer.write_all(header.as_bytes()).await.unwrap(); + writer.write_all(body).await.unwrap(); + writer.flush().await.unwrap(); +} + +async fn read_framed(reader: &mut (impl AsyncRead + Unpin)) -> Value { + let mut header = String::new(); + loop { + let mut byte = [0u8; 1]; + tokio::io::AsyncReadExt::read_exact(reader, &mut byte) + .await + .unwrap(); + header.push(byte[0] as char); + if header.ends_with("\r\n\r\n") { + break; + } + } + let length: usize = header + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut buf = vec![0u8; length]; + tokio::io::AsyncReadExt::read_exact(reader, &mut buf) + .await + .unwrap(); + serde_json::from_slice(&buf).unwrap() +} + +struct FakeServer { + read: tokio::io::DuplexStream, + write: tokio::io::DuplexStream, +} + +impl FakeServer { + async fn read_request(&mut self) -> Value { + timeout(TIMEOUT, read_framed(&mut self.read)).await.unwrap() + } + + async fn expect_quiet(&mut self) { + assert!( + timeout(QUIET, read_framed(&mut self.read)).await.is_err(), + "expected no wire traffic" + ); + } + + async fn respond(&mut self, request: &Value, result: Value) { + let id = request["id"].as_u64().unwrap(); + let response = json!({ "jsonrpc": "2.0", "id": id, "result": result }); + write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; + } + + async fn respond_error(&mut self, request: &Value, code: i64, message: &str) { + let id = request["id"].as_u64().unwrap(); + let response = json!({ + "jsonrpc": "2.0", + "id": id, + "error": { "code": code, "message": message }, + }); + write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; + } + + async fn send_event(&mut self, session_id: &str, id: &str, event_type: &str, ephemeral: bool) { + let notification = json!({ + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": session_id, + "event": { + "id": id, + "timestamp": "2025-01-01T00:00:00Z", + "ephemeral": ephemeral, + "type": event_type, + "data": {}, + }, + }, + }); + write_framed(&mut self.write, &serde_json::to_vec(¬ification).unwrap()).await; + } + + /// Emit the startup burst the host cares about: `BURST` ordered events + /// followed by an ephemeral `session.idle` that `getMessages` could + /// never recover. + async fn send_startup_burst(&mut self, session_id: &str) { + for i in 0..BURST { + self.send_event( + session_id, + &format!("evt-{i}"), + "assistant.message_delta", + false, + ) + .await; + } + self.send_event(session_id, "evt-idle", "session.idle", true) + .await; + } + + /// Answer the best-effort `session.skills.reload` that follows a resume. + async fn answer_skills_reload(&mut self) { + let request = self.read_request().await; + assert_eq!(request["method"], "session.skills.reload"); + self.respond(&request, json!({})).await; + } +} + +/// Minimal MCP-auth handler: its presence is what makes the SDK register +/// `mcp.oauth_required` interest after create/resume, which is the branch +/// under test. It is never invoked by these tests. +struct CancelMcpAuthHandler; + +#[async_trait] +impl McpAuthHandler for CancelMcpAuthHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _request: McpAuthRequest, + ) -> McpAuthResult { + McpAuthResult::Cancelled + } +} + +fn make_client() -> (Client, FakeServer) { + let (client_write, server_read) = duplex(1 << 20); + let (server_write, client_read) = duplex(1 << 20); + let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + ( + client, + FakeServer { + read: server_read, + write: server_write, + }, + ) +} + +fn cloud_options() -> CloudSessionOptions { + CloudSessionOptions::with_repository(CloudSessionRepository::new("octocat", "hello-world")) +} + +fn create_result(session_id: &str) -> Value { + json!({ "sessionId": session_id, "workspacePath": "/tmp/workspace" }) +} + +/// Collect the startup burst, asserting each event arrives exactly once and +/// in emission order. +async fn expect_startup_burst(events: &mut EventSubscription) { + for i in 0..BURST { + let event = timeout(TIMEOUT, events.recv()) + .await + .unwrap_or_else(|_| panic!("timed out waiting for event {i}")) + .unwrap_or_else(|error| panic!("event {i} not delivered: {error}")); + assert_eq!(event.id.as_str(), format!("evt-{i}"), "out-of-order event"); + } + let idle = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!(idle.id.as_str(), "evt-idle"); + assert_eq!(idle.event_type, "session.idle"); + assert_eq!(idle.ephemeral, Some(true)); +} + +/// Unwrap the error arm of a result whose `Ok` type is not `Debug`. +fn expect_error(result: Result) -> github_copilot_sdk::Error { + match result { + Ok(_) => panic!("expected an error"), + Err(error) => error, + } +} + +/// Assert the router holds exactly one registration, and that it is +/// `session_id`. +/// +/// The failure message is a fixed string: session IDs are never written to +/// test output. +fn assert_only_registration(client: &Client, session_id: &SessionId, context: &str) { + let registered = client.registered_session_ids_for_test(); + let matches_expected = registered.len() == 1 && registered[0] == *session_id; + assert!(matches_expected, "{context}"); +} + +/// Poll (bounded) until the client's router has no registered sessions. +/// +/// Diagnostics report how many registrations are outstanding rather than +/// which ones: session IDs are not written to test output. +async fn await_no_registrations(client: &Client) { + let deadline = tokio::time::Instant::now() + TIMEOUT; + loop { + let outstanding = client.registered_session_count_for_test(); + if outstanding == 0 { + return; + } + assert!( + tokio::time::Instant::now() < deadline, + "{outstanding} session registration(s) were never cleaned up" + ); + tokio::task::yield_now().await; + } +} + +/// Assert the subscription is closed (producer gone), tolerating any events +/// buffered before the close. +async fn expect_closed(events: &mut EventSubscription) { + loop { + match timeout(TIMEOUT, events.recv()).await.unwrap() { + Ok(_) => continue, + Err(error) => { + assert!( + matches!(error.kind(), RecvErrorKind::Closed), + "expected Closed, got {:?}", + error.kind() + ); + return; + } + } + } +} + +// --------------------------------------------------------------------------- +// 1 + 4. Loss-free startup events on create +// --------------------------------------------------------------------------- + +/// Subscription installed before `start()` is polled, drained concurrently: +/// the full pre-response burst plus the ephemeral `session.idle` arrives. +#[tokio::test] +async fn prepared_create_delivers_pre_response_burst_to_concurrent_drain() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-concurrent"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(2048), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let drain = tokio::spawn(async move { + expect_startup_burst(&mut events).await; + }); + + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + server.send_startup_burst(session_id.as_str()).await; + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + timeout(TIMEOUT, drain).await.unwrap().unwrap(); + drop(session); +} + +/// A large configured buffer retains the whole burst even when the consumer +/// does not read anything until `start()` has returned. +#[tokio::test] +async fn prepared_create_retains_burst_for_deferred_consumer() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-deferred"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(2048), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server.send_startup_burst(session_id.as_str()).await; + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + // Only now does the consumer start reading. + expect_startup_burst(&mut events).await; + drop(session); +} + +// --------------------------------------------------------------------------- +// 2. Loss-free startup events on resume +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn prepared_resume_delivers_pre_response_burst() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-resume"); + + let prepared = client + .prepare_resume_session( + ResumeSessionConfig::new(session_id.clone()) + .with_continue_pending_work(true) + .with_event_buffer_capacity(2048), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let resume_req = server.read_request().await; + assert_eq!(resume_req["method"], "session.resume"); + assert_eq!(resume_req["params"]["continuePendingWork"], true); + server.send_startup_burst(session_id.as_str()).await; + server + .respond(&resume_req, json!({ "sessionId": session_id.as_str() })) + .await; + server.answer_skills_reload().await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + expect_startup_burst(&mut events).await; + drop(session); +} + +// --------------------------------------------------------------------------- +// 3. Lag is observable, never silent +// --------------------------------------------------------------------------- + +/// An undersized buffer with a consumer that does not drain surfaces +/// `Lagged` rather than silently losing events, and the live tail stays +/// consumable afterwards. +#[tokio::test] +async fn undersized_buffer_reports_lag_and_keeps_live_tail() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-lag"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(8), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server.send_startup_burst(session_id.as_str()).await; + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + + // Drain until lag is reported. Every delivered event is still in order, + // and the loss is explicit rather than silent. + let mut lagged = None; + let mut last_index: Option = None; + while lagged.is_none() { + match timeout(TIMEOUT, events.recv()).await.unwrap() { + Ok(event) => { + if let Some(index) = event.id.as_str().strip_prefix("evt-") + && let Ok(index) = index.parse::() + { + if let Some(previous) = last_index { + assert!(index > previous, "delivered events must stay ordered"); + } + last_index = Some(index); + } + } + Err(error) => match error.kind() { + RecvErrorKind::Lagged(lag) => lagged = Some(lag.skipped()), + other => panic!("expected lag, got {other:?}"), + }, + } + } + assert!(lagged.unwrap() > 0, "lag must report the skipped count"); + + // The live tail is still consumable after a lag. + server + .send_event(session_id.as_str(), "evt-live", "assistant.message", false) + .await; + let live = loop { + match timeout(TIMEOUT, events.recv()).await.unwrap() { + Ok(event) if event.id.as_str() == "evt-live" => break event, + Ok(_) => continue, + Err(error) => match error.kind() { + RecvErrorKind::Lagged(_) => continue, + other => panic!("subscription ended before the live tail: {other:?}"), + }, + } + }; + assert_eq!(live.event_type, "assistant.message"); + drop(session); +} + +// --------------------------------------------------------------------------- +// 5 + 6. Inertness before start, and drop of an unstarted handle +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn prepare_is_inert_until_start_is_polled() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-inert"); + + let tasks_before = tokio::runtime::Handle::current() + .metrics() + .num_alive_tasks(); + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let _events = prepared.subscribe(); + + // No wire traffic, no router registration, no spawned task. + server.expect_quiet().await; + assert!(client.registered_session_ids_for_test().is_empty()); + assert_eq!( + tokio::runtime::Handle::current() + .metrics() + .num_alive_tasks(), + tasks_before, + "prepare must not spawn a task" + ); + + // Constructing the future is still inert; only polling it does work. + let start = prepared.start(); + server.expect_quiet().await; + assert!(client.registered_session_ids_for_test().is_empty()); + + let start = tokio::spawn(start); + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + drop(session); +} + +#[tokio::test] +async fn dropping_unstarted_prepared_session_leaves_no_state() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-dropped"); + + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + drop(prepared); + + assert!(matches!( + timeout(TIMEOUT, events.recv()) + .await + .unwrap() + .unwrap_err() + .kind(), + RecvErrorKind::Closed + )); + assert!(client.registered_session_ids_for_test().is_empty()); + server.expect_quiet().await; +} + +// --------------------------------------------------------------------------- +// 7. Cancelling a polled startup +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn cancelled_prepared_create_cleans_up_and_allows_retry() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-cancel"); + + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + // The request is on the wire; cancel before responding. + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + start.abort(); + let _ = start.await; + + await_no_registrations(&client).await; + expect_closed(&mut events).await; + + // A retry with the same session ID succeeds. + let retry = tokio::spawn( + client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap() + .start(), + ); + let retry_req = server.read_request().await; + assert_eq!(retry_req["method"], "session.create"); + server + .respond(&retry_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, retry).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id(), &session_id); + drop(session); +} + +#[tokio::test] +async fn cancelled_prepared_resume_cleans_up_and_allows_retry() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-resume-cancel"); + + let prepared = client + .prepare_resume_session(ResumeSessionConfig::new(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let resume_req = server.read_request().await; + assert_eq!(resume_req["method"], "session.resume"); + start.abort(); + let _ = start.await; + + await_no_registrations(&client).await; + expect_closed(&mut events).await; + + let retry = tokio::spawn( + client + .prepare_resume_session(ResumeSessionConfig::new(session_id.clone())) + .unwrap() + .start(), + ); + let retry_req = server.read_request().await; + assert_eq!(retry_req["method"], "session.resume"); + server + .respond(&retry_req, json!({ "sessionId": session_id.as_str() })) + .await; + server.answer_skills_reload().await; + let session = timeout(TIMEOUT, retry).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id(), &session_id); + drop(session); +} + +// --------------------------------------------------------------------------- +// 8. Startup failures preserve error kinds and clean up +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn create_rpc_error_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-rpc-error"); + + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server + .respond_error(&create_req, -32000, "session create failed") + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + assert!( + matches!(error.kind(), ErrorKind::Rpc { code: -32000 }), + "unexpected error kind: {:?}", + error.kind() + ); + await_no_registrations(&client).await; + expect_closed(&mut events).await; +} + +#[tokio::test] +async fn create_session_id_mismatch_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-mismatch"); + + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server + .respond(&create_req, create_result("some-other-id")) + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + let ErrorKind::Session(SessionErrorKind::SessionIdMismatch { + requested, + returned, + }) = error.kind() + else { + panic!("unexpected error kind: {:?}", error.kind()); + }; + assert_eq!(requested, &session_id); + assert_eq!(returned.as_str(), "some-other-id"); + + await_no_registrations(&client).await; + expect_closed(&mut events).await; +} + +#[tokio::test] +async fn resume_session_id_mismatch_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-resume-mismatch"); + + let prepared = client + .prepare_resume_session(ResumeSessionConfig::new(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let resume_req = server.read_request().await; + server + .respond(&resume_req, json!({ "sessionId": "another-session" })) + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + assert!( + matches!( + error.kind(), + ErrorKind::Session(SessionErrorKind::SessionIdMismatch { .. }) + ), + "unexpected error kind: {:?}", + error.kind() + ); + await_no_registrations(&client).await; + expect_closed(&mut events).await; +} + +/// The MCP-auth interest registration that follows a successful +/// `session.create` is the last fallible step before the session handle is +/// handed out. When it fails, the startup must unwind exactly like any +/// other create failure: original error kind preserved, router +/// registration removed, and subscriptions taken before `start()` closed. +#[tokio::test] +async fn create_mcp_auth_interest_error_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-interest-error"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_mcp_auth_handler(Arc::new(CancelMcpAuthHandler)), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + + let interest_req = server.read_request().await; + assert_eq!(interest_req["method"], "session.eventLog.registerInterest"); + assert_eq!(interest_req["params"]["eventType"], "mcp.oauth_required"); + server + .respond_error(&interest_req, -32003, "interest registration failed") + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + assert!( + matches!(error.kind(), ErrorKind::Rpc { code: -32003 }), + "unexpected error kind: {:?}", + error.kind() + ); + expect_closed(&mut events).await; + await_no_registrations(&client).await; +} + +/// The resume counterpart. Interest registration runs before the +/// best-effort `session.skills.reload`, so a failure must abort the +/// startup without issuing the reload. +#[tokio::test] +async fn resume_mcp_auth_interest_error_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-resume-interest-error"); + + let prepared = client + .prepare_resume_session( + ResumeSessionConfig::new(session_id.clone()) + .with_mcp_auth_handler(Arc::new(CancelMcpAuthHandler)), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let resume_req = server.read_request().await; + assert_eq!(resume_req["method"], "session.resume"); + server + .respond(&resume_req, json!({ "sessionId": session_id.as_str() })) + .await; + + let interest_req = server.read_request().await; + assert_eq!(interest_req["method"], "session.eventLog.registerInterest"); + server + .respond_error(&interest_req, -32004, "interest registration failed") + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + assert!( + matches!(error.kind(), ErrorKind::Rpc { code: -32004 }), + "unexpected error kind: {:?}", + error.kind() + ); + server.expect_quiet().await; + expect_closed(&mut events).await; + await_no_registrations(&client).await; +} + +#[tokio::test] +async fn zero_event_buffer_capacity_is_invalid_config() { + let (client, _server) = make_client(); + + let error = expect_error( + client.prepare_session(SessionConfig::default().with_event_buffer_capacity(0)), + ); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); + + let error = expect_error(client.prepare_resume_session( + ResumeSessionConfig::new(SessionId::new("zero")).with_event_buffer_capacity(0), + )); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); + + // The compatibility wrappers surface the same error. + let error = expect_error( + client + .create_session(SessionConfig::default().with_event_buffer_capacity(0)) + .await, + ); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); +} + +// --------------------------------------------------------------------------- +// 9. Early and late subscribers share one event loop +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn early_and_late_subscribers_share_one_event_loop() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-two-subscribers"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(2048), + ) + .unwrap(); + let mut early = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server + .send_event(session_id.as_str(), "evt-early", "assistant.message", false) + .await; + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + + let mut late = session.subscribe(); + server + .send_event(session_id.as_str(), "evt-late", "assistant.message", false) + .await; + + // The early subscriber sees both events, once each. + assert_eq!( + timeout(TIMEOUT, early.recv()) + .await + .unwrap() + .unwrap() + .id + .as_str(), + "evt-early" + ); + assert_eq!( + timeout(TIMEOUT, early.recv()) + .await + .unwrap() + .unwrap() + .id + .as_str(), + "evt-late" + ); + // The late subscriber only sees what was emitted after it subscribed — + // exactly once, which would be twice if a second event loop existed. + assert_eq!( + timeout(TIMEOUT, late.recv()) + .await + .unwrap() + .unwrap() + .id + .as_str(), + "evt-late" + ); + assert!( + timeout(QUIET, late.recv()).await.is_err(), + "duplicate delivery implies more than one event loop" + ); + assert!( + timeout(QUIET, early.recv()).await.is_err(), + "duplicate delivery implies more than one event loop" + ); + drop(session); +} + +// --------------------------------------------------------------------------- +// 10. Compatibility wrappers +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn create_session_wrapper_keeps_rpc_sequence() { + let (client, mut server) = make_client(); + + let start = tokio::spawn({ + let client = client.clone(); + async move { client.create_session(SessionConfig::default()).await } + }); + + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + let session_id = create_req["params"]["sessionId"] + .as_str() + .unwrap() + .to_string(); + server + .respond(&create_req, create_result(&session_id)) + .await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id().as_str(), session_id); + server.expect_quiet().await; + drop(session); +} + +#[tokio::test] +async fn resume_session_wrapper_keeps_rpc_sequence() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("wrapper-resume"); + + let start = tokio::spawn({ + let client = client.clone(); + let session_id = session_id.clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id)) + .await + } + }); + + let resume_req = server.read_request().await; + assert_eq!(resume_req["method"], "session.resume"); + assert_eq!(resume_req["params"]["sessionId"], session_id.as_str()); + server + .respond(&resume_req, json!({ "sessionId": session_id.as_str() })) + .await; + server.answer_skills_reload().await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id(), &session_id); + server.expect_quiet().await; + drop(session); +} + +// --------------------------------------------------------------------------- +// 11. Type-level guarantees +// --------------------------------------------------------------------------- + +/// Detects `Clone` without requiring it: the inherent method wins whenever +/// `T: Clone`, otherwise the blanket trait method is selected. +struct CloneProbe(PhantomData); + +impl CloneProbe { + fn is_clone(&self) -> bool { + true + } +} + +trait MaybeClone { + fn is_clone(&self) -> bool { + false + } +} + +impl MaybeClone for CloneProbe {} + +#[test] +fn prepared_session_is_send_static_and_not_clone() { + fn assert_send_static() {} + assert_send_static::(); + + // Sanity-check the probe against a type that is `Clone` ... + assert!(CloneProbe::(PhantomData).is_clone()); + // ... then assert `PreparedSession` deliberately is not, so a prepared + // session can never be started twice. + assert!(!CloneProbe::(PhantomData).is_clone()); +} + +// --------------------------------------------------------------------------- +// Registration ownership: a stale startup guard must never unregister a +// newer registration that reused the same session ID. +// --------------------------------------------------------------------------- + +/// Drive a startup future until it parks awaiting its RPC response. +/// +/// The duration is a bound, not a correctness sleep: whether the future +/// actually reached the wire is asserted afterwards by reading the request, +/// which fails loudly on its own timeout if it did not. +const DRIVE: Duration = Duration::from_millis(50); + +#[tokio::test] +async fn stale_create_guard_does_not_unregister_same_id_retry() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("stale-create-guard"); + + // First attempt: registers, sends `session.create`, then parks. + let mut first = Box::pin( + client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap() + .start(), + ); + let _ = timeout(DRIVE, &mut first).await; + let first_req = server.read_request().await; + assert_eq!(first_req["method"], "session.create"); + + // Second attempt with the same pinned ID, started before the first is + // dropped, so it replaces the first attempt's router registration. + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(64), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let mut second = Box::pin(prepared.start()); + let _ = timeout(DRIVE, &mut second).await; + let second_req = server.read_request().await; + assert_eq!(second_req["method"], "session.create"); + + // The stale guard runs now. It must not touch the live registration. + drop(first); + assert_only_registration( + &client, + &session_id, + "a stale startup guard unregistered the live retry", + ); + + server + .respond(&second_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, &mut second).await.unwrap().unwrap(); + + // Events must still route to the surviving registration. + server + .send_event( + session_id.as_str(), + "evt-after-stale", + "assistant.message", + false, + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!(event.id.as_str(), "evt-after-stale"); + drop(session); +} + +#[tokio::test] +async fn stale_resume_guard_does_not_unregister_same_id_retry() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("stale-resume-guard"); + + let mut first = Box::pin( + client + .prepare_resume_session(ResumeSessionConfig::new(session_id.clone())) + .unwrap() + .start(), + ); + let _ = timeout(DRIVE, &mut first).await; + let first_req = server.read_request().await; + assert_eq!(first_req["method"], "session.resume"); + + let prepared = client + .prepare_resume_session( + ResumeSessionConfig::new(session_id.clone()).with_event_buffer_capacity(64), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let mut second = Box::pin(prepared.start()); + let _ = timeout(DRIVE, &mut second).await; + let second_req = server.read_request().await; + assert_eq!(second_req["method"], "session.resume"); + + drop(first); + assert_only_registration( + &client, + &session_id, + "a stale startup guard unregistered the live retry", + ); + + // Hand the surviving startup to a task: resume issues a follow-up + // `session.skills.reload` that only makes progress while it is polled. + let second = tokio::spawn(second); + server + .respond(&second_req, json!({ "sessionId": session_id.as_str() })) + .await; + server.answer_skills_reload().await; + let session = timeout(TIMEOUT, second).await.unwrap().unwrap().unwrap(); + + server + .send_event( + session_id.as_str(), + "evt-after-stale", + "assistant.message", + false, + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!(event.id.as_str(), "evt-after-stale"); + drop(session); +} + +/// A disconnected session must not unregister a same-ID session that +/// replaced it. +#[tokio::test] +async fn dropping_superseded_session_does_not_unregister_its_replacement() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("superseded-session"); + + let first = tokio::spawn( + client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap() + .start(), + ); + let first_req = server.read_request().await; + server + .respond(&first_req, create_result(session_id.as_str())) + .await; + let first_session = timeout(TIMEOUT, first).await.unwrap().unwrap().unwrap(); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(64), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let second = tokio::spawn(prepared.start()); + let second_req = server.read_request().await; + server + .respond(&second_req, create_result(session_id.as_str())) + .await; + let second_session = timeout(TIMEOUT, second).await.unwrap().unwrap().unwrap(); + + // The superseded handle goes away; the live session must survive. + drop(first_session); + assert_only_registration( + &client, + &session_id, + "dropping a superseded Session unregistered its replacement", + ); + + server + .send_event( + session_id.as_str(), + "evt-survivor", + "assistant.message", + false, + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!(event.id.as_str(), "evt-survivor"); + drop(second_session); +} + +// --------------------------------------------------------------------------- +// Deferred (server-assigned ID) create cancellation +// --------------------------------------------------------------------------- + +/// Cancelling a cloud create before the response arrives must leave no +/// registration behind, even though the session ID is only known to the +/// inline response callback. +#[tokio::test] +async fn cancelled_deferred_create_leaves_no_registration() { + let (client, mut server) = make_client(); + + let prepared = client + .prepare_session(SessionConfig::default().with_cloud(cloud_options())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + assert!(create_req["params"]["sessionId"].is_null()); + + // Cancel before the server answers, then answer: the response carries + // the server-assigned ID the inline callback would register. + start.abort(); + let _ = start.await; + server + .respond(&create_req, create_result("server-assigned-id")) + .await; + + expect_closed(&mut events).await; + await_no_registrations(&client).await; + // Nothing may appear after the response has been fully processed. + server.expect_quiet().await; + assert!( + client.registered_session_ids_for_test().is_empty(), + "a cancelled deferred create left a registration behind" + ); + + // A fresh cloud create still works afterwards. + let retry = tokio::spawn( + client + .prepare_session(SessionConfig::default().with_cloud(cloud_options())) + .unwrap() + .start(), + ); + let retry_req = server.read_request().await; + server + .respond(&retry_req, create_result("server-assigned-retry")) + .await; + let session = timeout(TIMEOUT, retry).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id().as_str(), "server-assigned-retry"); + drop(session); +} + +/// Poll (bounded) until `session_id` shows up on the client's router. +/// +/// The failure message names the expectation, not the ID: session IDs are +/// not written to test output. +async fn await_registered(client: &Client, session_id: &str) { + let deadline = tokio::time::Instant::now() + TIMEOUT; + while !client + .registered_session_ids_for_test() + .iter() + .any(|id| id.as_str() == session_id) + { + assert!( + tokio::time::Instant::now() < deadline, + "inline callback never registered the expected session" + ); + tokio::task::yield_now().await; + } +} + +/// The other half of the deferred-create window: cancellation lands *after* +/// the inline response callback has already registered the server-assigned +/// ID. The startup guard owns that registration and must remove it. +/// +/// Deterministic by construction — the start future is parked on its +/// response and never polled again, so the callback (which runs on the +/// JSON-RPC read task, independently of the caller) is guaranteed to have +/// registered before the future is dropped. +#[tokio::test] +async fn deferred_create_cancelled_after_callback_registered_is_cleaned_up() { + let (client, mut server) = make_client(); + + let prepared = client + .prepare_session(SessionConfig::default().with_cloud(cloud_options())) + .unwrap(); + let mut events = prepared.subscribe(); + let mut start = Box::pin(prepared.start()); + + // Drive to the wire, then park. + let _ = timeout(DRIVE, &mut start).await; + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + + // The read task runs the inline callback and registers the ID while the + // caller's future stays unpolled. + server + .respond(&create_req, create_result("registered-then-cancelled")) + .await; + await_registered(&client, "registered-then-cancelled").await; + + // Cancellation now lands on a slot that already owns a registration. + drop(start); + + expect_closed(&mut events).await; + await_no_registrations(&client).await; + server.expect_quiet().await; + + // The same server-assigned ID can be handed out again without the dead + // attempt's cleanup interfering. + let retry = tokio::spawn( + client + .prepare_session(SessionConfig::default().with_cloud(cloud_options())) + .unwrap() + .start(), + ); + let retry_req = server.read_request().await; + server + .respond(&retry_req, create_result("registered-then-cancelled")) + .await; + let session = timeout(TIMEOUT, retry).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id().as_str(), "registered-then-cancelled"); + assert_eq!( + client.registered_session_count_for_test(), + 1, + "retry must hold exactly one registration" + ); + drop(session); +} From fcffcdf0e986f452da0346682e27ee8fcb450f83 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 4 Sep 2026 04:36:21 -0700 Subject: [PATCH 60/60] Use session detach for SDK cleanup (#2307) Add real-runtime cold resume coverage across SDKs so disconnect preserves persisted sessions across client/runtime restart. Co-authored-by: Steve Sanderson Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Session.cs | 22 ++++-- dotnet/test/E2E/ClientLifecycleE2ETests.cs | 2 +- dotnet/test/E2E/SessionE2ETests.cs | 44 ++++++++++++ dotnet/test/Harness/E2ETestBase.cs | 2 +- dotnet/test/Harness/E2ETestContext.cs | 2 +- .../test/Unit/ClientSessionLifetimeTests.cs | 6 +- dotnet/test/Unit/GitHubTelemetryTests.cs | 2 +- go/client_test.go | 4 +- go/github_token_provider_test.go | 10 +-- go/internal/e2e/client_options_e2e_test.go | 4 ++ go/internal/e2e/session_e2e_test.go | 58 +++++++++++++++ go/session.go | 16 ++++- go/types.go | 9 ++- .../com/github/copilot/CopilotSession.java | 22 +++++- .../github/copilot/ClientOptionsE2ETest.java | 2 + .../com/github/copilot/CopilotClientTest.java | 4 +- .../github/copilot/CopilotSessionTest.java | 45 ++++++++++++ .../github/copilot/GitHubTelemetryTest.java | 3 +- .../com/github/copilot/McpAndAgentsTest.java | 2 +- .../McpAuthInterestRegistrationTest.java | 5 +- .../copilot/SessionEventHandlingTest.java | 4 +- .../github/copilot/TimeoutEdgeCaseTest.java | 49 +++++++++---- .../copilot/ZeroTimeoutContractTest.java | 6 +- nodejs/src/session.ts | 15 +++- nodejs/test/e2e/client.e2e.test.ts | 2 +- nodejs/test/e2e/client_options.e2e.test.ts | 5 ++ nodejs/test/e2e/session.e2e.test.ts | 53 ++++++++++++++ nodejs/test/github-token-provider.test.ts | 2 +- python/copilot/session.py | 21 +++--- python/e2e/test_client_options_e2e.py | 4 ++ python/e2e/test_session_e2e.py | 52 ++++++++++++++ python/test_github_token_provider.py | 4 +- rust/src/errors.rs | 6 +- rust/src/lib.rs | 48 ++++++++----- rust/src/session.rs | 13 +--- rust/tests/e2e/client_options.rs | 4 ++ rust/tests/e2e/session.rs | 70 +++++++++++++++++++ rust/tests/session_test.rs | 29 ++++---- ..._cold_resume_with_explicit_session_id.yaml | 16 +++++ 39 files changed, 563 insertions(+), 104 deletions(-) create mode 100644 test/snapshots/session/should_recover_marker_after_cold_resume_with_explicit_session_id.yaml diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 42cbe3f9ce..b01432a5ff 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -2109,8 +2109,12 @@ public async ValueTask DisposeAsync() try { - await InvokeRpcAsync( - "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None); + var response = await InvokeRpcAsync( + "session.detach", [new SessionDetachRequest() { SessionId = SessionId }], CancellationToken.None); + if (!response.Success) + { + LogSessionDetachFailed(SessionId, response.Error ?? "unknown error"); + } } catch (ObjectDisposedException) { @@ -2147,6 +2151,9 @@ await InvokeRpcAsync( [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to fetch tool metadata for {toolName}")] private partial void LogToolMetadataFetchFailed(Exception exception, string toolName); + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to detach session {sessionId}: {error}")] + private partial void LogSessionDetachFailed(string sessionId, string error); + [LoggerMessage(Level = LogLevel.Error, Message = "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}")] private partial void LogPermissionHandlerOrDeliveryFailed(Exception exception, string sessionId, string requestId); @@ -2184,11 +2191,17 @@ internal record SessionAbortRequest public string SessionId { get; init; } = string.Empty; } - internal record SessionDestroyRequest + internal record SessionDetachRequest { public string SessionId { get; init; } = string.Empty; } + internal record SessionDetachResponse + { + public bool Success { get; init; } + public string? Error { get; init; } + } + internal void ThrowIfDisposed() { ObjectDisposedException.ThrowIf(Volatile.Read(ref _isDisposed) != 0, this); @@ -2221,7 +2234,8 @@ internal void ThrowIfDisposed() [JsonSerializable(typeof(SendMessageRequest))] [JsonSerializable(typeof(SendMessageResponse))] [JsonSerializable(typeof(SessionAbortRequest))] - [JsonSerializable(typeof(SessionDestroyRequest))] + [JsonSerializable(typeof(SessionDetachRequest))] + [JsonSerializable(typeof(SessionDetachResponse))] [JsonSerializable(typeof(SessionEndHookInput))] [JsonSerializable(typeof(SessionEndHookOutput))] [JsonSerializable(typeof(SessionStartHookInput))] diff --git a/dotnet/test/E2E/ClientLifecycleE2ETests.cs b/dotnet/test/E2E/ClientLifecycleE2ETests.cs index 4b09c695d4..82b2d2badf 100644 --- a/dotnet/test/E2E/ClientLifecycleE2ETests.cs +++ b/dotnet/test/E2E/ClientLifecycleE2ETests.cs @@ -121,7 +121,7 @@ public async Task Should_Receive_Session_Deleted_Lifecycle_Event_When_Deleted() } }); - // Do NOT DisposeAsync the session before deleting: dispose sends session.destroy + // Do NOT DisposeAsync the session before deleting: dispose sends session.detach // which closes in-memory state but does not remove the disk file; calling // delete afterwards still succeeds, but skipping dispose keeps the test minimal. await Client.DeleteSessionAsync(sessionId); diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs index aababb670b..fab84bc439 100644 --- a/dotnet/test/E2E/SessionE2ETests.cs +++ b/dotnet/test/E2E/SessionE2ETests.cs @@ -295,6 +295,50 @@ public async Task Resumes_A_Persisted_Session_From_A_New_Client_When_An_Mcp_OAut Assert.Equal(sessionId, session2.SessionId); } + [Fact] + public async Task Should_Recover_Marker_After_Cold_Resume_With_Explicit_Session_Id() + { + await using var isolatedCtx = await E2ETestContext.CreateAsync(); + await isolatedCtx.ConfigureForTestAsync("session", nameof(Should_Recover_Marker_After_Cold_Resume_With_Explicit_Session_Id)); + + var sessionId = $"e2e-cold-resume-{Guid.NewGuid()}"; + + var client1 = isolatedCtx.CreateClient(); + var session1 = await isolatedCtx.CreateSessionAsync(client1, new SessionConfig + { + SessionId = sessionId, + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + Assert.Equal(sessionId, session1.SessionId); + + var answer = await session1.SendAndWaitAsync(new MessageOptions + { + Prompt = "Please remember this exact secret marker for later - MARKER-7f3ac21e. Reply with only the single word \"Acknowledged\".", + }); + Assert.NotNull(answer); + Assert.Contains("Acknowledged", answer!.Data.Content ?? string.Empty); + + await session1.DisposeAsync(); + await client1.ForceStopAsync(); + + var client2 = isolatedCtx.CreateClient(); + var session2 = await isolatedCtx.ResumeSessionAsync(client2, sessionId, new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + Assert.Equal(sessionId, session2.SessionId); + + var answer2 = await session2.SendAndWaitAsync(new MessageOptions + { + Prompt = "What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing else.", + }); + Assert.NotNull(answer2); + Assert.Contains("MARKER-7f3ac21e", answer2!.Data.Content ?? string.Empty); + + await session2.DisposeAsync(); + await client2.ForceStopAsync(); + } + [Fact] public async Task Should_Throw_Error_When_Resuming_Non_Existent_Session() { diff --git a/dotnet/test/Harness/E2ETestBase.cs b/dotnet/test/Harness/E2ETestBase.cs index 3eb0f0e97a..f664812d58 100644 --- a/dotnet/test/Harness/E2ETestBase.cs +++ b/dotnet/test/Harness/E2ETestBase.cs @@ -113,7 +113,7 @@ protected static async Task SuspendAndUntrackSessionForResumeAsync(CopilotSessio { await session.Rpc.SuspendAsync(); - // In-process clients host separate runtimes, while session.destroy removes the + // In-process clients host separate runtimes, while session.detach releases the // session from the current runtime. Untrack locally to exercise resume without // either replacing an active wrapper or destroying the session first. var removeFromClient = typeof(CopilotSession).GetMethod( diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs index fcba56e70a..8d474bfe7e 100644 --- a/dotnet/test/Harness/E2ETestContext.cs +++ b/dotnet/test/Harness/E2ETestContext.cs @@ -595,7 +595,7 @@ private static async Task StopClientForCleanupAsync(CopilotClient client) $"Graceful in-process client cleanup exceeded {s_gracefulClientStopTimeout}; forcing shutdown."); await client.ForceStopAsync(); - // Disposing the connection completes any session.destroy RPC that + // Disposing the connection completes any session.detach RPC that // blocked graceful cleanup. Observe that task before continuing. await gracefulStop.WaitAsync(s_gracefulClientStopTimeout); } diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index 20c25f4885..7029d1ea0e 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -1950,7 +1950,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel { ["success"] = true }, - "session.destroy" => await DestroySessionAsync(cancellationToken), + "session.detach" => await DetachSessionAsync(cancellationToken), "runtime.shutdown" => HandleRuntimeShutdown(), _ => throw new InvalidOperationException($"Unexpected RPC method '{method}'.") }; @@ -1987,7 +1987,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel }; } - private async Task> DestroySessionAsync(CancellationToken cancellationToken) + private async Task> DetachSessionAsync(CancellationToken cancellationToken) { if (_delayDestroy) { @@ -1995,7 +1995,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel await _allowDestroy.Task.WaitAsync(cancellationToken); } - return []; + return new Dictionary { ["success"] = true }; } private Dictionary HandleRuntimeShutdown() diff --git a/dotnet/test/Unit/GitHubTelemetryTests.cs b/dotnet/test/Unit/GitHubTelemetryTests.cs index d3e7672da0..6213e0b1d0 100644 --- a/dotnet/test/Unit/GitHubTelemetryTests.cs +++ b/dotnet/test/Unit/GitHubTelemetryTests.cs @@ -495,7 +495,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel "session.create" => CaptureCreate(request), "session.resume" => CaptureResume(request), "session.send" => new Dictionary { ["messageId"] = "message-1" }, - "session.destroy" => new Dictionary(), + "session.detach" => new Dictionary { ["success"] = true }, "session.options.update" => new Dictionary { ["success"] = true }, "runtime.shutdown" => new Dictionary(), _ => throw new InvalidOperationException($"Unexpected RPC method '{method}'."), diff --git a/go/client_test.go b/go/client_test.go index b573bd3dca..f8415be6be 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -2917,8 +2917,10 @@ func serveInMemoryRuntime(t *testing.T, stdinR *io.PipeReader, stdoutW *io.PipeW result = map[string]any{"id": "interest-1"} case "session.options.update": result = map[string]any{"success": true} - case "session.skills.reload", "session.destroy": + case "session.skills.reload": result = map[string]any{} + case "session.detach": + result = map[string]any{"success": true} default: t.Errorf("unexpected JSON-RPC method %s", request.Method) return diff --git a/go/github_token_provider_test.go b/go/github_token_provider_test.go index f00837379f..51e8f3f7f0 100644 --- a/go/github_token_provider_test.go +++ b/go/github_token_provider_test.go @@ -48,8 +48,8 @@ func TestGitHubTokenProviderCreateRequestAndCallback(t *testing.T) { sessionID := sessionIDFromParams(t, params) return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil }) - server.SetRequestHandler("session.destroy", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - return []byte(`{}`), nil + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil }) var gotArgs GitHubTokenProviderArgs @@ -201,8 +201,8 @@ func TestGitHubTokenStringRedactsAccessToken(t *testing.T) { func TestGitHubTokenProviderCleanupOnDisconnectError(t *testing.T) { rpcClient, server, _ := newRuntimeShutdownRpcPair(t) t.Cleanup(server.Stop) - server.SetRequestHandler("session.destroy", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - return nil, &jsonrpc2.Error{Code: -32000, Message: "destroy failed"} + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "detach failed"} }) client := &Client{} registrationID := client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { @@ -213,7 +213,7 @@ func TestGitHubTokenProviderCleanupOnDisconnectError(t *testing.T) { client.unregisterGitHubTokenProvider(registrationID) }) - if err := session.Disconnect(); err == nil || !strings.Contains(err.Error(), "destroy failed") { + if err := session.Disconnect(); err == nil || !strings.Contains(err.Error(), "detach failed") { t.Fatalf("Disconnect error = %v", err) } if len(client.gitHubTokenProviders) != 0 { diff --git a/go/internal/e2e/client_options_e2e_test.go b/go/internal/e2e/client_options_e2e_test.go index 9c7b77c984..54461443ca 100644 --- a/go/internal/e2e/client_options_e2e_test.go +++ b/go/internal/e2e/client_options_e2e_test.go @@ -869,6 +869,10 @@ function handleMessage(message) { writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null }); return; } + if (message.method === "session.detach") { + writeResponse(message.id, { success: true }); + return; + } if (message.method === "session.resume") { const sessionId = (message.params && message.params.sessionId) || "fake-session"; writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null }); diff --git a/go/internal/e2e/session_e2e_test.go b/go/internal/e2e/session_e2e_test.go index efc20250b1..12550e6e2d 100644 --- a/go/internal/e2e/session_e2e_test.go +++ b/go/internal/e2e/session_e2e_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "github.com/google/uuid" + copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" "github.com/github/copilot-sdk/go/rpc" @@ -526,6 +528,62 @@ func TestSessionE2E(t *testing.T) { } }) + t.Run("should recover marker after cold resume with explicit session id", func(t *testing.T) { + ctx.ConfigureForTest(t) + + sessionID := "e2e-cold-resume-" + uuid.NewString() + + client1 := ctx.NewClient() + session1, err := client1.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SessionID: sessionID, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + if session1.SessionID != sessionID { + t.Fatalf("Expected explicit session ID %q, got %q", sessionID, session1.SessionID) + } + + answer, err := session1.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: `Please remember this exact secret marker for later - MARKER-7f3ac21e. Reply with only the single word "Acknowledged".`, + }) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + if ad, ok := answer.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(ad.Content, "Acknowledged") { + t.Errorf("Expected answer to contain 'Acknowledged', got %v", answer.Data) + } + + if err := session1.Disconnect(); err != nil { + t.Fatalf("Failed to disconnect session: %v", err) + } + client1.ForceStop() + + client2 := ctx.NewClient() + defer client2.ForceStop() + + session2, err := client2.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to resume session: %v", err) + } + if session2.SessionID != sessionID { + t.Errorf("Expected resumed session ID to match, got %q vs %q", session2.SessionID, sessionID) + } + + answer2, err := session2.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing else.", + }) + if err != nil { + t.Fatalf("Failed to send message after resume: %v", err) + } + if ad, ok := answer2.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(ad.Content, "MARKER-7f3ac21e") { + t.Errorf("Expected resumed answer to contain marker, got %v", answer2.Data) + } + }) + t.Run("should throw error when resuming non-existent session", func(t *testing.T) { ctx.ConfigureForTest(t) diff --git a/go/session.go b/go/session.go index 058fb60a1e..cb64bbc609 100644 --- a/go/session.go +++ b/go/session.go @@ -1741,8 +1741,22 @@ func (s *Session) GetEvents(ctx context.Context) ([]SessionEvent, error) { // log.Printf("Failed to disconnect session: %v", err) // } func (s *Session) Disconnect() error { - _, err := s.client.Request(context.Background(), "session.destroy", sessionDestroyRequest{SessionID: s.SessionID}) + result, err := s.client.Request(context.Background(), "session.detach", sessionDetachRequest{SessionID: s.SessionID}) + if err == nil { + var response sessionDetachResponse + if decodeErr := json.Unmarshal(result, &response); decodeErr != nil { + err = fmt.Errorf("failed to decode session detach response: %w", decodeErr) + } else if !response.Success { + if response.Error == "" { + response.Error = "unknown error" + } + err = errors.New(response.Error) + } + } + // Local cleanup always runs, even if the detach RPC failed, so callers + // don't leak in-memory resources (event goroutines, registered + // providers/handlers) just because the runtime couldn't be reached. s.stopEventProcessing() s.releaseGitHubTokenProviderRegistration() diff --git a/go/types.go b/go/types.go index b4a34f2561..dfedc95e71 100644 --- a/go/types.go +++ b/go/types.go @@ -2893,11 +2893,16 @@ type sessionGetMessagesResponse struct { Events []SessionEvent `json:"events"` } -// sessionDestroyRequest is the request for session.destroy -type sessionDestroyRequest struct { +// sessionDetachRequest is the request for session.detach. +type sessionDetachRequest struct { SessionID string `json:"sessionId"` } +type sessionDetachResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + // sessionAbortRequest is the request for session.abort type sessionAbortRequest struct { SessionID string `json:"sessionId"` diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index 156cb3495a..55a83137b4 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -2432,10 +2432,20 @@ public void close() { timeoutScheduler.shutdownNow(); releaseGitHubTokenProviderRegistration(); + RuntimeException detachFailure = null; try { - rpc.invoke("session.destroy", Map.of("sessionId", sessionId), Void.class).get(5, TimeUnit.SECONDS); + SessionDetachResponse response = rpc + .invoke("session.detach", Map.of("sessionId", sessionId), SessionDetachResponse.class) + .get(5, TimeUnit.SECONDS); + if (response == null || !response.success()) { + String detail = response != null && response.error() != null ? response.error() : "unknown error"; + detachFailure = new IllegalStateException("Failed to detach session " + sessionId + ": " + detail); + } } catch (Exception e) { - LOG.log(Level.FINE, "Error destroying session", e); + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + detachFailure = new IllegalStateException("Failed to detach session " + sessionId, e); } eventHandlers.clear(); @@ -2447,10 +2457,18 @@ public void close() { exitPlanModeHandler.set(null); autoModeSwitchHandler.set(null); hooksHandler.set(null); + + if (detachFailure != null) { + throw detachFailure; + } } // ===== Internal response types for agent API ===== + @JsonIgnoreProperties(ignoreUnknown = true) + record SessionDetachResponse(@JsonProperty("success") boolean success, @JsonProperty("error") String error) { + } + @JsonIgnoreProperties(ignoreUnknown = true) private record AgentListResponse(@JsonProperty("agents") List agents) { } diff --git a/java/sdk/src/test/java/com/github/copilot/ClientOptionsE2ETest.java b/java/sdk/src/test/java/com/github/copilot/ClientOptionsE2ETest.java index 45056afdb4..a2561434d8 100644 --- a/java/sdk/src/test/java/com/github/copilot/ClientOptionsE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/ClientOptionsE2ETest.java @@ -263,6 +263,8 @@ function resultFor(message) { return { sessionId: message.params?.sessionId ?? 'fake-session', openCanvases: [] }; case 'session.resume': return { sessionId: message.params?.sessionId ?? 'fake-session', openCanvases: [] }; + case 'session.detach': + return { success: true }; case 'session.options.update': return { success: true }; default: diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java index db40d425f4..ba161ef998 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java @@ -120,8 +120,8 @@ void testDeleteSessionReleasesGitHubTokenProvider() throws Exception { var rpc = mock(JsonRpcClient.class); when(rpc.invoke(eq("session.delete"), any(), eq(DeleteSessionResponse.class))) .thenReturn(CompletableFuture.completedFuture(new DeleteSessionResponse(true, null))); - when(rpc.invoke(eq("session.destroy"), any(), eq(Void.class))) - .thenReturn(CompletableFuture.completedFuture(null)); + when(rpc.invoke(eq("session.detach"), any(), eq(CopilotSession.SessionDetachResponse.class))) + .thenReturn(CompletableFuture.completedFuture(new CopilotSession.SessionDetachResponse(true, null))); setConnectionFuture(client, rpc, null); var registry = new GitHubTokenProviderRegistry(); diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java index eb061b029d..667769fab2 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java @@ -352,6 +352,51 @@ void testShouldResumeSessionUsingNewClient() throws Exception { } } + @Test + @Tag("isolated-resume") + void testShouldRecoverMarkerAfterColdResumeWithExplicitSessionId() throws Exception { + final String snapshot = "should_recover_marker_after_cold_resume_with_explicit_session_id"; + ctx.configureForTest("session", snapshot); + + String sessionId = "e2e-cold-resume-" + java.util.UUID.randomUUID(); + + try (CopilotClient client1 = ctx.createClient()) { + CopilotSession session1 = client1.createSession( + new SessionConfig().setSessionId(sessionId).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + assertEquals(sessionId, session1.getSessionId()); + + AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions() + .setPrompt("Please remember this exact secret marker for later - MARKER-7f3ac21e. " + + "Reply with only the single word \"Acknowledged\".")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("Acknowledged"), + "Response should contain Acknowledged: " + answer.getData().content()); + + session1.close(); + client1.forceStop().get(30, TimeUnit.SECONDS); + } + + try (CopilotClient client2 = ctx.createClient()) { + CopilotSession session2 = client2 + .resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + assertEquals(sessionId, session2.getSessionId()); + + AssistantMessageEvent answer2 = session2.sendAndWait( + new MessageOptions().setPrompt("What was the exact secret marker I asked you to remember earlier? " + + "Reply with only that marker value and nothing else.")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer2); + assertTrue(answer2.getData().content().contains("MARKER-7f3ac21e"), + "Resumed response should contain marker: " + answer2.getData().content()); + + session2.close(); + } + } + /** * Verifies that sessions work with appended system message configuration. * diff --git a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java index e344b4f748..10c47eee86 100644 --- a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java +++ b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java @@ -366,7 +366,8 @@ private void acceptLoop() { respond(rpc, id, Map.of("sessionId", params.path("sessionId").asText("resume-1"), "workspacePath", "/workspace")); }); - rpc.registerMethodHandler("session.destroy", (id, params) -> respond(rpc, id, Map.of())); + rpc.registerMethodHandler("session.detach", + (id, params) -> respond(rpc, id, Map.of("success", true))); rpc.registerMethodHandler("runtime.shutdown", (id, params) -> respond(rpc, id, Map.of())); }); ready.complete(server); diff --git a/java/sdk/src/test/java/com/github/copilot/McpAndAgentsTest.java b/java/sdk/src/test/java/com/github/copilot/McpAndAgentsTest.java index 06d9dca39e..018204d030 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpAndAgentsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpAndAgentsTest.java @@ -451,7 +451,7 @@ void testShouldAcceptDefaultAgentConfigurationOnSessionResume() throws Exception assertNotNull(session.getSessionId()); String sessionId = session.getSessionId(); - // Do not call session.close() here — that invokes session.destroy on the + // Do not call session.close() here — that invokes session.detach on the // server, // which removes the session and causes the subsequent resumeSession to fail // with "Session not found". The session handle is simply abandoned and the diff --git a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java index 06ac08a2a4..b83cedc70a 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java @@ -255,7 +255,10 @@ private static JsonNode resultFor(String method, JsonNode params) { } case "session.eventLog.registerInterest" -> result.put("id", "interest-1"); case "session.options.update" -> result.put("success", true); - case "session.skills.reload", "session.destroy" -> { + case "session.skills.reload" -> { + } + case "session.detach" -> { + result.put("success", true); } default -> throw new IllegalStateException("Unexpected RPC method " + method); } diff --git a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java index b75e710720..5e502459c3 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java @@ -96,8 +96,8 @@ void testSendAndWaitSkipsAutopilotContinuationIdle() throws Exception { var rpc = mock(JsonRpcClient.class); when(rpc.invoke(eq("session.send"), any(), eq(SendMessageResponse.class))) .thenReturn(CompletableFuture.completedFuture(new SendMessageResponse("message-1"))); - when(rpc.invoke(eq("session.destroy"), any(), eq(Void.class))) - .thenReturn(CompletableFuture.completedFuture(null)); + when(rpc.invoke(eq("session.detach"), any(), eq(CopilotSession.SessionDetachResponse.class))) + .thenReturn(CompletableFuture.completedFuture(new CopilotSession.SessionDetachResponse(true, null))); session = new CopilotSession("test-session-id", rpc); try { diff --git a/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java b/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java index 17e1851bb4..01530bc25f 100644 --- a/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java +++ b/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java @@ -10,7 +10,11 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; @@ -34,29 +38,46 @@ public class TimeoutEdgeCaseTest { /** - * Creates a {@link JsonRpcClient} whose {@code invoke()} returns futures that - * never complete. The reader thread blocks forever on the input stream, and - * writes go to a no-op output stream. + * Creates a {@link JsonRpcClient} whose prompt requests never complete but + * whose cleanup detach request succeeds. */ private JsonRpcClient createHangingRpcClient() throws Exception { - InputStream blockingInput = new InputStream() { + PipedInputStream input = new PipedInputStream(); + PipedOutputStream responseWriter = new PipedOutputStream(input); + OutputStream sinkOutput = new ByteArrayOutputStream() { @Override - public int read() throws IOException { + public synchronized void flush() throws IOException { + super.flush(); + // Each JsonRpcClient.sendMessage() call writes exactly one + // complete message before calling flush(), so the buffer must + // be cleared after every flush; otherwise a later request's + // "id" lookup can match a stale, already-processed message + // still sitting in the buffer. try { - Thread.sleep(Long.MAX_VALUE); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return -1; + String request = toString(StandardCharsets.UTF_8); + if (!request.contains("\"method\":\"session.detach\"")) { + return; + } + + int idIndex = request.indexOf("\"id\":"); + int idEnd = request.indexOf(",", idIndex); + String id = request.substring(idIndex + "\"id\":".length(), idEnd); + String response = "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"success\":true}}"; + byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8); + responseWriter.write( + ("Content-Length: " + responseBytes.length + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); + responseWriter.write(responseBytes); + responseWriter.flush(); + } finally { + reset(); } - return -1; } }; - ByteArrayOutputStream sinkOutput = new ByteArrayOutputStream(); var ctor = JsonRpcClient.class.getDeclaredConstructor(InputStream.class, java.io.OutputStream.class, Socket.class, Process.class); ctor.setAccessible(true); - return (JsonRpcClient) ctor.newInstance(blockingInput, sinkOutput, null, null); + return (JsonRpcClient) ctor.newInstance(input, sinkOutput, null, null); } /** @@ -64,7 +85,7 @@ public int read() throws IOException { * completed by a stale timeout. *

* Contract: {@code close()} shuts down the timeout scheduler before the - * blocking {@code session.destroy} RPC call, so any pending timeout task is + * blocking {@code session.detach} RPC call, so any pending timeout task is * cancelled and the future remains incomplete (not exceptionally completed with * {@code TimeoutException}). */ @@ -79,7 +100,7 @@ void testTimeoutDoesNotFireAfterSessionClose() throws Exception { assertFalse(result.isDone(), "Future should be pending before timeout fires"); - // close() blocks up to 5s on session.destroy RPC. The 2s timeout + // close() blocks up to 5s on session.detach RPC. The 2s timeout // fires during that window with the current per-call scheduler. session.close(); diff --git a/java/sdk/src/test/java/com/github/copilot/ZeroTimeoutContractTest.java b/java/sdk/src/test/java/com/github/copilot/ZeroTimeoutContractTest.java index 3d986566dc..25a0e892cc 100644 --- a/java/sdk/src/test/java/com/github/copilot/ZeroTimeoutContractTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ZeroTimeoutContractTest.java @@ -31,9 +31,9 @@ void sendAndWaitWithZeroTimeoutShouldNotTimeOut() throws Exception { var mockRpc = mock(JsonRpcClient.class); when(mockRpc.invoke(any(), any(), any())).thenAnswer(invocation -> { Object method = invocation.getArgument(0); - if ("session.destroy".equals(method)) { - // Make session.close() non-blocking by completing destroy immediately - return CompletableFuture.completedFuture(null); + if ("session.detach".equals(method)) { + // Make session.close() non-blocking by completing detach immediately + return CompletableFuture.completedFuture(new CopilotSession.SessionDetachResponse(true, null)); } // For other calls (e.g., message send), return an incomplete future so the // sendAndWait result does not complete due to a mock response. diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 0dfff88bc7..d68517f822 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -2019,10 +2019,19 @@ export class CopilotSession { if (this.disconnected) { return; } - await this.connection.sendRequest("session.destroy", { - sessionId: this.sessionId, - }); + let response: { success: boolean; error?: string } = { success: false }; + for (let attempt = 0; attempt < 2 && !response.success; attempt++) { + response = (await this.connection.sendRequest("session.detach", { + sessionId: this.sessionId, + })) as { success: boolean; error?: string }; + } + if (!response.success) { + throw new Error( + `Failed to disconnect session ${this.sessionId}: ${response.error || "Unknown error"}` + ); + } this._markDisconnected(); + this.onDisconnected?.(); } /** Enables `await using session = ...` syntax for automatic cleanup. */ diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts index bc3421bfa1..a529ea8e4c 100644 --- a/nodejs/test/e2e/client.e2e.test.ts +++ b/nodejs/test/e2e/client.e2e.test.ts @@ -107,7 +107,7 @@ describe("Client", () => { expect(errors[0].message).toContain("Failed to disconnect session"); } }, - // Generous timeout: client.stop() must wait for session.destroy to time out + // Generous timeout: client.stop() must wait for session.detach to time out // when the server process is dead. The default 30s can flake on slow CI under load. 60_000 ); diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index 2f1dee6cb5..4d261bea52 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -99,6 +99,11 @@ function handleMessage(message) { return; } + if (message.method === "session.detach") { + writeResponse(message.id, { success: true }); + return; + } + if (message.method === "session.resume") { const sessionId = message.params?.sessionId ?? message.params?.[0]?.sessionId ?? "fake-session"; writeResponse(message.id, { diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index bab0efc687..4c20acb345 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -95,6 +95,59 @@ describe("Sessions", () => { await resumedSession.disconnect(); await originalSession.disconnect(); }); + + it("should recover marker after cold resume with explicit session id", async () => { + const sessionId = `e2e-resume-${Date.now()}`; + const marker = "MARKER-7f3ac21e"; + const firstClient = new CopilotClient({ + workingDirectory: workDir, + env, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + }); + onTestFinished(async () => { + try { + await firstClient.stop(); + } catch { + // ignore + } + }); + + const session = await firstClient.createSession({ + sessionId, + onPermissionRequest: approveAll, + model: "claude-sonnet-4.5", + }); + await session.sendAndWait({ + prompt: `Please remember this exact secret marker for later - ${marker}. Reply with only the single word "Acknowledged".`, + }); + await session.disconnect(); + await firstClient.stop(); + + const secondClient = new CopilotClient({ + workingDirectory: workDir, + env, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + }); + onTestFinished(async () => { + try { + await secondClient.stop(); + } catch { + // ignore + } + }); + const resumedSession = await secondClient.resumeSession(sessionId, { + onPermissionRequest: approveAll, + model: "claude-sonnet-4.5", + }); + const response = await resumedSession.sendAndWait({ + prompt: "What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing else.", + }); + + expect(response?.data.content).toContain(marker); + await resumedSession.disconnect(); + await secondClient.stop(); + }); + it("should create and disconnect sessions", async () => { await using session = await client.createSession({ onPermissionRequest: approveAll, diff --git a/nodejs/test/github-token-provider.test.ts b/nodejs/test/github-token-provider.test.ts index 2202f466a1..661b05c39f 100644 --- a/nodejs/test/github-token-provider.test.ts +++ b/nodejs/test/github-token-provider.test.ts @@ -177,7 +177,7 @@ describe("session GitHub token providers", () => { const client = createMockClient(async (method, params) => { if (method === "session.create") return { sessionId: params.sessionId }; - if (method === "session.destroy") return {}; + if (method === "session.detach") return { success: true }; if (method === "session.delete") return { success: true }; throw new Error(`Unexpected method: ${method}`); }); diff --git a/python/copilot/session.py b/python/copilot/session.py index d92fc7f34f..20c470302e 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1635,6 +1635,7 @@ def __init__( self._open_canvases_lock = threading.Lock() self._rpc: SessionRpc | None = None self._destroyed = False + self._disconnect_lock = asyncio.Lock() self._on_disconnect = on_disconnect def _set_disconnect_callback(self, callback: Callable[[], None]) -> None: @@ -3009,19 +3010,19 @@ async def disconnect(self) -> None: >>> # Clean up when done — session can still be resumed later >>> await session.disconnect() """ - # Ensure that the check and update of _destroyed are atomic so that - # only the first caller proceeds to send the destroy RPC. - with self._event_handlers_lock: - if self._destroyed: - return - self._destroyed = True + async with self._disconnect_lock: + with self._event_handlers_lock: + if self._destroyed: + return + + response = await self._client.request("session.detach", {"sessionId": self.session_id}) + if not response.get("success"): + detail = response.get("error") or "unknown error" + raise RuntimeError(f"Failed to detach session {self.session_id}: {detail}") - try: - await self._client.request("session.destroy", {"sessionId": self.session_id}) - finally: self._run_disconnect_callback() - # Clear handlers even if the request fails. with self._event_handlers_lock: + self._destroyed = True self._event_handlers.clear() with self._tool_handlers_lock: self._tool_handlers.clear() diff --git a/python/e2e/test_client_options_e2e.py b/python/e2e/test_client_options_e2e.py index 4bceabcb5e..b07e9a5402 100644 --- a/python/e2e/test_client_options_e2e.py +++ b/python/e2e/test_client_options_e2e.py @@ -168,6 +168,10 @@ def _get_available_port() -> int: writeResponse(message.id, { success: true }); return; } + if (message.method === "session.detach") { + writeResponse(message.id, { success: true }); + return; + } writeResponse(message.id, {}); } diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py index a126eabc40..f57b9f5736 100644 --- a/python/e2e/test_session_e2e.py +++ b/python/e2e/test_session_e2e.py @@ -2,6 +2,7 @@ import base64 import os +import uuid from datetime import datetime import pytest @@ -315,6 +316,57 @@ def on_mcp_auth_request(_request, _invocation): finally: await new_client.force_stop() + async def test_should_recover_marker_after_cold_resume_with_explicit_session_id( + self, ctx: E2ETestContext + ): + session_id = f"e2e-cold-resume-{uuid.uuid4()}" + github_token = DEFAULT_GITHUB_TOKEN if os.environ.get("GITHUB_ACTIONS") == "true" else None + + client1 = CopilotClient( + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + ) + try: + session1 = await client1.create_session( + on_permission_request=PermissionHandler.approve_all, + session_id=session_id, + ) + assert session1.session_id == session_id + + answer = await session1.send_and_wait( + "Please remember this exact secret marker for later - MARKER-7f3ac21e. " + 'Reply with only the single word "Acknowledged".' + ) + assert answer is not None + assert "Acknowledged" in answer.data.content + + await session1.disconnect() + finally: + await client1.force_stop() + + client2 = CopilotClient( + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + ) + try: + session2 = await client2.resume_session( + session_id, on_permission_request=PermissionHandler.approve_all + ) + assert session2.session_id == session_id + + answer2 = await session2.send_and_wait( + "What was the exact secret marker I asked you to remember earlier? " + "Reply with only that marker value and nothing else." + ) + assert answer2 is not None + assert "MARKER-7f3ac21e" in answer2.data.content + finally: + await client2.force_stop() + async def test_should_throw_error_resuming_nonexistent_session(self, ctx: E2ETestContext): with pytest.raises(Exception): await ctx.client.resume_session( diff --git a/python/test_github_token_provider.py b/python/test_github_token_provider.py index 5b203f3e34..9566a4f817 100644 --- a/python/test_github_token_provider.py +++ b/python/test_github_token_provider.py @@ -31,8 +31,8 @@ async def request(self, method: str, params: dict[str, Any], **kwargs: Any) -> d if callback is not None: callback(response) return response - if method == "session.destroy": - return {} + if method == "session.detach": + return {"success": True} if method == "session.delete": return {"success": True} raise RuntimeError(f"Unexpected method: {method}") diff --git a/rust/src/errors.rs b/rust/src/errors.rs index 70f4c14ff1..3bf5becbda 100644 --- a/rust/src/errors.rs +++ b/rust/src/errors.rs @@ -152,6 +152,9 @@ pub enum SessionErrorKind { /// Session ID returned by the CLI. returned: SessionId, }, + + /// The CLI could not detach the session. + DetachFailed, } impl fmt::Display for SessionErrorKind { @@ -186,6 +189,7 @@ impl fmt::Display for SessionErrorKind { f, "CLI returned session ID {returned} after SDK registered {requested}" ), + SessionErrorKind::DetachFailed => write!(f, "failed to detach session"), } } } @@ -400,7 +404,7 @@ fn capture_backtrace() -> Option> { /// /// `Client::stop` performs cooperative shutdown across every active /// session before killing the CLI child process. Errors from any -/// per-session `session.destroy` RPC and from the terminal child-kill +/// per-session `session.detach` RPC and from the terminal child-kill /// step are collected here rather than short-circuiting on the first /// failure, so callers see the full picture of what went wrong during /// teardown. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 01d5dec5a4..7d15d8f35c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -66,6 +66,12 @@ pub mod session_events; /// [`Client::rpc`] and [`session::Session::rpc`](crate::session::Session::rpc). pub mod rpc; +#[derive(serde::Deserialize)] +struct SessionDetachResponse { + success: bool, + error: Option, +} + // Auto-generated protocol-type modules. Crate-private so the only public // access path is via the `session_events` and `rpc` facade modules above — // callers can never depend on the implementation-detail layout under @@ -2259,6 +2265,25 @@ impl Client { self.call_with_inline_callback(method, params, None).await } + pub(crate) async fn detach_session(&self, session_id: &str) -> Result<()> { + let value = self + .call( + "session.detach", + Some(serde_json::json!({ "sessionId": session_id })), + ) + .await?; + let response: SessionDetachResponse = serde_json::from_value(value)?; + if response.success { + return Ok(()); + } + Err(Error::with_message( + ErrorKind::Session(SessionErrorKind::DetachFailed), + response + .error + .unwrap_or_else(|| "unknown error".to_string()), + )) + } + /// Same as [`call`](Self::call), but installs an `inline_callback` /// that runs synchronously on the JSON-RPC read task the instant the /// successful response is parsed, before it is delivered to this @@ -2646,12 +2671,7 @@ impl Client { let mut first_error = None; for session_id in self.inner.router.session_ids() { - if let Err(error) = self - .call( - "session.destroy", - Some(serde_json::json!({ "sessionId": session_id })), - ) - .await + if let Err(error) = self.detach_session(&session_id).await && first_error.is_none() { first_error = Some(error); @@ -2777,10 +2797,10 @@ impl Client { /// Cooperatively shut down the client and the CLI child process. /// - /// Walks every still-registered session and sends `session.destroy` + /// Walks every still-registered session and sends `session.detach` /// for each one, asks SDK-owned runtimes to shut down, terminates the /// Windows-owned CLI Job Object when present, and reaps the root process. - /// Errors from per-session destroys, runtime shutdown, and final process + /// Errors from per-session detaches, runtime shutdown, and final process /// termination are collected into [`StopErrors`] rather than /// short-circuiting on the first failure — so callers see the full picture /// of teardown. @@ -2809,21 +2829,15 @@ impl Client { self.inner.extension_launch_provider.clear(); // Snapshot the registered session IDs without holding the router - // lock across the destroy RPCs. + // lock across the detach RPCs. for session_id in self.inner.router.session_ids() { - match self - .call( - "session.destroy", - Some(serde_json::json!({ "sessionId": session_id })), - ) - .await - { + match self.detach_session(&session_id).await { Ok(_) => {} Err(e) => { warn!( session_id = %session_id, error = %e, - "session.destroy failed during Client::stop", + "session.detach failed during Client::stop", ); errors.push(e); } diff --git a/rust/src/session.rs b/rust/src/session.rs index f935beca99..8e0ac31eaf 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -723,7 +723,7 @@ impl Session { /// Disconnect this session from the CLI. /// - /// Sends the `session.destroy` RPC, stops the event loop, and unregisters + /// Sends the `session.detach` RPC, stops the event loop, and unregisters /// the session from the client. **Session state on disk** (conversation /// history, planning state, artifacts) is **preserved**, so the /// conversation can be resumed later via [`Client::resume_session`] @@ -738,12 +738,7 @@ impl Session { /// [`Client::delete_session`]: crate::Client::delete_session /// [`send_and_wait`]: Self::send_and_wait pub async fn disconnect(&self) -> Result<(), Error> { - self.client - .call( - "session.destroy", - Some(serde_json::json!({ "sessionId": self.id })), - ) - .await?; + self.client.detach_session(&self.id).await?; self.stop_event_loop().await; self.github_token_registration.lock().take(); self.client @@ -751,9 +746,7 @@ impl Session { Ok(()) } - /// Deprecated alias for [`disconnect`](Self::disconnect). The - /// underlying wire RPC happens to be named `session.destroy`, but it - /// only severs the connection — on-disk session state is preserved. + /// Deprecated alias for [`disconnect`](Self::disconnect). /// Prefer `disconnect` in new code. #[deprecated(since = "0.1.0", note = "Use `disconnect()` instead")] pub async fn destroy(&self) -> Result<(), Error> { diff --git a/rust/tests/e2e/client_options.rs b/rust/tests/e2e/client_options.rs index fa8e6f68f7..51880803d3 100644 --- a/rust/tests/e2e/client_options.rs +++ b/rust/tests/e2e/client_options.rs @@ -491,6 +491,10 @@ function handleMessage(message) { writeResponse(message.id, { success: true }); return; } + if (message.method === "session.detach") { + writeResponse(message.id, { success: true }); + return; + } writeResponse(message.id, {}); } diff --git a/rust/tests/e2e/session.rs b/rust/tests/e2e/session.rs index 04c99422ea..02a13e7810 100644 --- a/rust/tests/e2e/session.rs +++ b/rust/tests/e2e/session.rs @@ -627,6 +627,76 @@ async fn should_resume_a_session_using_a_new_client() { .await; } +#[tokio::test] +async fn should_recover_marker_after_cold_resume_with_explicit_session_id() { + super::support::with_dedicated_e2e_context( + "session", + "should_recover_marker_after_cold_resume_with_explicit_session_id", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + + let session_id = SessionId::from(format!( + "e2e-cold-resume-{}", + uuid::Uuid::new_v4().simple() + )); + + let client1 = ctx.start_client().await; + let session1 = client1 + .create_session( + ctx.approve_all_session_config() + .with_session_id(session_id.clone()), + ) + .await + .expect("create session"); + assert_eq!(session1.id(), &session_id); + + let first = session1 + .send_and_wait( + "Please remember this exact secret marker for later - MARKER-7f3ac21e. Reply with only the single word \"Acknowledged\".", + ) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&first).contains("Acknowledged")); + + session1 + .disconnect() + .await + .expect("disconnect first session"); + client1.stop().await.expect("stop first client"); + + let new_client = ctx.start_client().await; + let resumed = new_client + .resume_session( + ResumeSessionConfig::new(session_id.clone()) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN), + ) + .await + .expect("resume session"); + assert_eq!(resumed.id(), &session_id); + + let second = resumed + .send_and_wait( + "What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing else.", + ) + .await + .expect("send after resume") + .expect("assistant message"); + assert!(assistant_message_content(&second).contains("MARKER-7f3ac21e")); + + resumed + .disconnect() + .await + .expect("disconnect resumed session"); + new_client.stop().await.expect("stop new client"); + }) + }, + ) + .await; +} + #[tokio::test] async fn resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured() { super::support::with_dedicated_e2e_context( diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 778e13e71f..ad4c8abe42 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -1884,7 +1884,7 @@ async fn session_rpc_methods_send_correct_method_names() { let cases: Vec<(&str, Option<&str>)> = vec![ ("session.abort", None), ("session.log", Some("message")), - ("session.destroy", None), + ("session.detach", None), ]; for (expected_method, extra_param_key) in cases { @@ -1893,7 +1893,7 @@ async fn session_rpc_methods_send_correct_method_names() { match expected_method { "session.abort" => s.abort().await.map(|_| ()), "session.log" => s.log("test msg", None).await, - "session.destroy" => s.disconnect().await, + "session.detach" => s.disconnect().await, _ => unreachable!(), } }); @@ -1911,6 +1911,7 @@ async fn session_rpc_methods_send_correct_method_names() { "session.log" => { serde_json::json!({ "eventId": "00000000-0000-0000-0000-000000000000" }) } + "session.detach" => serde_json::json!({ "success": true }), _ => serde_json::json!({}), }; server.respond(&request, response).await; @@ -4841,7 +4842,7 @@ async fn rpc_namespace_client_models_list_dispatches_correctly() { #[tokio::test] async fn client_stop_sends_session_destroy_for_each_active_session() { // One client, two registered sessions. Client::stop must send - // session.destroy for each before returning Ok. + // session.detach for each before returning Ok. let (client, server_read, server_write) = make_client(); let mut server = FakeServer { @@ -4891,31 +4892,33 @@ async fn client_stop_sends_session_destroy_for_each_active_session() { .await; let _session_b = timeout(TIMEOUT, create_b).await.unwrap(); - // Drive Client::stop and respond to each destroy in turn. + // Drive Client::stop and respond to each detach in turn. let stop_handle = tokio::spawn({ let client = client.clone(); async move { client.stop().await } }); - let mut destroyed = Vec::new(); + let mut detached = Vec::new(); for _ in 0..2 { let req = server.read_request().await; - assert_eq!(req["method"], "session.destroy"); - destroyed.push(req["params"]["sessionId"].as_str().unwrap().to_string()); - server.respond(&req, serde_json::json!(null)).await; + assert_eq!(req["method"], "session.detach"); + detached.push(req["params"]["sessionId"].as_str().unwrap().to_string()); + server + .respond(&req, serde_json::json!({ "success": true })) + .await; } - destroyed.sort(); + detached.sort(); let mut expected = [session_id_a.clone(), session_id_b.clone()]; expected.sort(); - assert_eq!(destroyed, expected); + assert_eq!(detached, expected); let stop_result = timeout(TIMEOUT, stop_handle).await.unwrap().unwrap(); assert!(stop_result.is_ok(), "stop returned errors: {stop_result:?}"); } #[tokio::test] -async fn client_stop_aggregates_session_destroy_errors() { - // session.destroy fails on the wire — Client::stop returns +async fn client_stop_aggregates_session_detach_errors() { + // session.detach fails on the wire — Client::stop returns // StopErrors carrying the failure rather than short-circuiting. let (session, mut server) = create_session_pair().await; let client = session.client().clone(); @@ -4923,7 +4926,7 @@ async fn client_stop_aggregates_session_destroy_errors() { let stop_handle = tokio::spawn(async move { client.stop().await }); let req = server.read_request().await; - assert_eq!(req["method"], "session.destroy"); + assert_eq!(req["method"], "session.detach"); let id = req["id"].as_u64().unwrap(); let response = serde_json::json!({ "jsonrpc": "2.0", diff --git a/test/snapshots/session/should_recover_marker_after_cold_resume_with_explicit_session_id.yaml b/test/snapshots/session/should_recover_marker_after_cold_resume_with_explicit_session_id.yaml new file mode 100644 index 0000000000..f862aa80eb --- /dev/null +++ b/test/snapshots/session/should_recover_marker_after_cold_resume_with_explicit_session_id.yaml @@ -0,0 +1,16 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Please remember this exact secret marker for later - MARKER-7f3ac21e. Reply with only the single word + "Acknowledged". + - role: assistant + content: Acknowledged + - role: user + content: What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing + else. + - role: assistant + content: MARKER-7f3ac21e