@@ -5458,7 +5476,26 @@ pub struct McpListToolsRequest {
pub server_name: String,
}
-/// MCP tool metadata with tool name and optional description.
+/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block.
+///
+///
+///
+/// **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 McpToolUi {
+ /// URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub resource_uri: Option
,
+ /// Tool visibility advertised by the server. When absent, MCP Apps defaults apply.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub visibility: Option>,
+}
+
+/// MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata.
///
///
///
@@ -5474,6 +5511,9 @@ pub struct McpTools {
pub description: Option
,
/// Tool name.
pub name: String,
+ /// Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` block was present without recognized fields.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub ui: Option,
}
/// Tools exposed by the connected MCP server. Throws when the server is not connected.
@@ -11657,6 +11697,21 @@ pub struct SessionMetadataSnapshot {
pub workspace_path: Option,
}
+/// Cost-category metadata for a CAPI model.
+///
+///
+///
+/// **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 SessionModelPriceCategory {
+ pub id: String,
+ pub price_category: ModelPickerPriceCategory,
+}
+
/// The list of models available to this session.
///
///
@@ -11670,6 +11725,9 @@ pub struct SessionMetadataSnapshot {
pub struct SessionModelList {
/// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`).
pub list: Vec
,
+ /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub model_price_categories: Option>,
/// Per-quota snapshots returned alongside the model list, keyed by quota type.
#[serde(skip_serializing_if = "Option::is_none")]
pub quota_snapshots: Option>,
@@ -13222,6 +13280,9 @@ pub struct SessionUpdateOptionsParams {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionUpdateOptionsResult {
+ /// Number of hooks loaded from installed plugins, returned when installedPlugins is updated
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub plugin_hook_count: Option,
/// Whether the operation succeeded
pub success: bool,
}
@@ -16514,6 +16575,9 @@ pub struct SessionModelSetReasoningEffortResult {
pub struct SessionModelListResult {
/// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`).
pub list: Vec,
+ /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub model_price_categories: Option>,
/// Per-quota snapshots returned alongside the model list, keyed by quota type.
#[serde(skip_serializing_if = "Option::is_none")]
pub quota_snapshots: Option>,
@@ -17914,6 +17978,9 @@ pub struct SessionProviderAddResult {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionOptionsUpdateResult {
+ /// Number of hooks loaded from installed plugins, returned when installedPlugins is updated
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub plugin_hook_count: Option,
/// Whether the operation succeeded
pub success: bool,
}
@@ -20781,6 +20848,63 @@ pub enum HMACAuthInfoType {
Hmac,
}
+/// 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 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 {
@@ -21205,6 +21329,28 @@ pub enum McpHeadersHandlePendingHeadersRefreshRequest {
None(McpHeadersHandlePendingHeadersRefreshRequestNone),
}
+/// Consumer allowed to call an MCP tool.
+///
+///
+///
+/// **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 McpToolUiVisibility {
+ /// The model may call the tool.
+ #[serde(rename = "model")]
+ Model,
+ /// An MCP App view may call the tool.
+ #[serde(rename = "app")]
+ App,
+ /// Unknown variant for forward compatibility.
+ #[default]
+ #[serde(other)]
+ Unknown,
+}
+
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum McpOauthPendingRequestResponseTokenKind {
#[serde(rename = "token")]
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs
index 64b663e59e..403933a27c 100644
--- a/rust/src/generated/rpc.rs
+++ b/rust/src/generated/rpc.rs
@@ -4349,7 +4349,7 @@ impl<'a> SessionRpcMcp<'a> {
Ok(serde_json::from_value(_value)?)
}
- /// Lists the tools exposed by a connected MCP server on this session's host.
+ /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session.
///
/// Wire method: `session.mcp.listTools`.
///
diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs
index aa2f1e3a2a..cf670c4856 100644
--- a/rust/src/generated/session_events.rs
+++ b/rust/src/generated/session_events.rs
@@ -77,6 +77,8 @@ pub enum SessionEventType {
AssistantTurnStart,
#[serde(rename = "assistant.intent")]
AssistantIntent,
+ #[serde(rename = "assistant.server_tool_progress")]
+ AssistantServerToolProgress,
#[serde(rename = "assistant.reasoning")]
AssistantReasoning,
#[serde(rename = "assistant.reasoning_delta")]
@@ -195,6 +197,15 @@ pub enum SessionEventType {
///
#[serde(rename = "session.auto_mode_resolved")]
SessionAutoModeResolved,
+ ///
+ ///
+ ///
+ /// **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.managed_settings_resolved")]
+ SessionManagedSettingsResolved,
#[serde(rename = "commands.changed")]
CommandsChanged,
#[serde(rename = "capabilities.changed")]
@@ -359,6 +370,8 @@ pub enum SessionEventData {
AssistantTurnStart(AssistantTurnStartData),
#[serde(rename = "assistant.intent")]
AssistantIntent(AssistantIntentData),
+ #[serde(rename = "assistant.server_tool_progress")]
+ AssistantServerToolProgress(AssistantServerToolProgressData),
#[serde(rename = "assistant.reasoning")]
AssistantReasoning(AssistantReasoningData),
#[serde(rename = "assistant.reasoning_delta")]
@@ -470,6 +483,15 @@ pub enum SessionEventData {
///
#[serde(rename = "session.auto_mode_resolved")]
SessionAutoModeResolved(SessionAutoModeResolvedData),
+ ///
+ ///
+ ///
+ /// **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.managed_settings_resolved")]
+ SessionManagedSettingsResolved(SessionManagedSettingsResolvedData),
#[serde(rename = "commands.changed")]
CommandsChanged(CommandsChangedData),
#[serde(rename = "capabilities.changed")]
@@ -1461,6 +1483,18 @@ pub struct AssistantIntentData {
pub intent: String,
}
+/// Session event "assistant.server_tool_progress". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AssistantServerToolProgressData {
+ /// Kind of hosted server tool that is running. Only `web_search` is emitted today.
+ pub kind: String,
+ /// Position of the hosted tool call in the response output. Stable across the call's lifecycle events (unlike the provider's per-event item id, which CAPI rotates), so the host keys the live in-progress row on it.
+ pub output_index: i64,
+ /// Lifecycle status of the hosted call: `in_progress`, `searching`, or `completed`.
+ pub status: String,
+}
+
/// Session event "assistant.reasoning". Assistant reasoning content for timeline display with complete thinking text
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -3653,6 +3687,29 @@ pub struct SamplingCompletedData {
pub request_id: RequestId,
}
+/// Single HTTP header entry as a name/value pair.
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct HeaderEntry {
+ /// HTTP response header name as observed by the runtime.
+ pub name: String,
+ /// HTTP response header value as observed by the runtime.
+ pub value: String,
+}
+
+/// Raw HTTP response details from the OAuth auth challenge, as observed by the runtime.
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct McpOauthHttpResponse {
+ /// Complete UTF-8 response body for host-specific challenge handling, including an empty string for an empty body. Omitted when the complete body is not valid UTF-8; body read failures fail the HTTP operation rather than exposing a partial response.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub body: Option,
+ /// HTTP response headers as observed by the runtime. Order and casing are transport-dependent, and duplicate header names may appear multiple times.
+ pub headers: Vec,
+ /// HTTP status code returned with the auth challenge.
+ pub status_code: i32,
+}
+
/// Static OAuth client configuration, if the server specifies one
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -3689,6 +3746,9 @@ pub struct McpOauthWWWAuthenticateParams {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpOauthRequiredData {
+ /// Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. Header order and casing are transport-dependent, and duplicate header names may appear multiple times.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub http_response: Option,
/// Why the runtime is requesting host-provided OAuth credentials.
pub reason: McpOauthRequestReason,
/// Unique identifier for this OAuth request; used to respond via session.mcp.oauth.handlePendingRequest
@@ -3916,6 +3976,34 @@ pub struct SessionAutoModeResolvedData {
pub reasoning_bucket: Option,
}
+/// Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. 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; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes.
+///
+///
+///
+/// **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 SessionManagedSettingsResolvedData {
+ /// Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true.
+ pub bypass_permissions_disabled: bool,
+ /// Whether the device (MDM/plist/registry/file) managed-settings layer was present
+ pub device_managed: bool,
+ /// 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.
+ pub fail_closed: bool,
+ /// The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force.
+ pub managed_keys: Vec,
+ /// Whether the server (account/org) managed-settings layer was present
+ pub server_managed: bool,
+ /// 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,
+ /// Which channel supplied the effective managed settings (the winning layer), or `none` when no policy is in force
+ pub source: ManagedSettingsResolvedSource,
+}
+
/// A single slash command available in the session, as listed by the `commands.changed` event.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -4119,7 +4207,7 @@ pub struct SessionMcpServerStatusChangedData {
pub status: McpServerStatus,
}
-/// Session event "mcp.tools.list_changed". Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed.
+/// 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")]
pub struct McpToolsListChangedData {
@@ -4127,7 +4215,7 @@ pub struct McpToolsListChangedData {
pub server_name: String,
}
-/// Session event "mcp.resources.list_changed". Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed.
+/// Session event "mcp.resources.list_changed". Payload identifying the MCP server associated with a list change.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpResourcesListChangedData {
@@ -4135,7 +4223,7 @@ pub struct McpResourcesListChangedData {
pub server_name: String,
}
-/// Session event "mcp.prompts.list_changed". Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed.
+/// Session event "mcp.prompts.list_changed". Payload identifying the MCP server associated with a list change.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpPromptsListChangedData {
@@ -5570,6 +5658,24 @@ pub enum AutoModeResolvedReasoningBucket {
Unknown,
}
+/// Which channel supplied the effective enterprise managed settings (highest-authority present layer wins wholesale)
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub enum ManagedSettingsResolvedSource {
+ /// Account/org policy self-fetched from the GitHub managed-settings endpoint (higher authority).
+ #[serde(rename = "server")]
+ Server,
+ /// Device-level MDM policy discovered from plist/registry/file (lower authority).
+ #[serde(rename = "device")]
+ Device,
+ /// No managed policy is in force (no layer contributed).
+ #[serde(rename = "none")]
+ None,
+ /// Unknown variant for forward compatibility.
+ #[default]
+ #[serde(other)]
+ Unknown,
+}
+
/// Exit plan mode action
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExitPlanModeAction {
diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json
index 6405fdcdd4..1a8462d661 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.71-2",
+ "@github/copilot": "^1.0.71",
"@modelcontextprotocol/sdk": "^1.26.0",
"@types/node": "^25.3.3",
"@types/node-forge": "^1.3.14",
@@ -501,9 +501,9 @@
}
},
"node_modules/@github/copilot": {
- "version": "1.0.71-2",
- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.71-2.tgz",
- "integrity": "sha512-En1onRCWjPy64wnjB9B6T6nXgPI7/myHksMotpKHzh8+CnVbaYQr2n/rBKM1BVScWgpA0zn19HmKZZlg82LW7A==",
+ "version": "1.0.71",
+ "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.71.tgz",
+ "integrity": "sha512-F3axBi+sXSLYDJbxCBW36bM6MYKNC2rlyAf3Ivo/MjiHKKJW7j5AmaR1IRYS9Gt8r9mxOwWFM1cJFA+CuLaR8g==",
"dev": true,
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
@@ -513,20 +513,20 @@
"copilot": "npm-loader.js"
},
"optionalDependencies": {
- "@github/copilot-darwin-arm64": "1.0.71-2",
- "@github/copilot-darwin-x64": "1.0.71-2",
- "@github/copilot-linux-arm64": "1.0.71-2",
- "@github/copilot-linux-x64": "1.0.71-2",
- "@github/copilot-linuxmusl-arm64": "1.0.71-2",
- "@github/copilot-linuxmusl-x64": "1.0.71-2",
- "@github/copilot-win32-arm64": "1.0.71-2",
- "@github/copilot-win32-x64": "1.0.71-2"
+ "@github/copilot-darwin-arm64": "1.0.71",
+ "@github/copilot-darwin-x64": "1.0.71",
+ "@github/copilot-linux-arm64": "1.0.71",
+ "@github/copilot-linux-x64": "1.0.71",
+ "@github/copilot-linuxmusl-arm64": "1.0.71",
+ "@github/copilot-linuxmusl-x64": "1.0.71",
+ "@github/copilot-win32-arm64": "1.0.71",
+ "@github/copilot-win32-x64": "1.0.71"
}
},
"node_modules/@github/copilot-darwin-arm64": {
- "version": "1.0.71-2",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.71-2.tgz",
- "integrity": "sha512-6DA1W3RFbyDPL/MXPeAzM9HxS7hvZOnToJHupGf4QGGs4dG2dzmTyEv0LkD4rFtyc1dx6QtyOjRt5vUsWw39Xw==",
+ "version": "1.0.71",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.71.tgz",
+ "integrity": "sha512-mEWzyqbqRAWgyU7i2uuSRoVPx/TwaFQX0nZmw0bc30aJ0BnO7cy2kYQyCHw8ykmf/tfxT0xauZ6k0BOFmWizzQ==",
"cpu": [
"arm64"
],
@@ -541,9 +541,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
- "version": "1.0.71-2",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.71-2.tgz",
- "integrity": "sha512-u/DWMhTCFaGf9TE70IgXKk0/9kWiijiHfEMVRqedbzdUNxGQ5u4lsaquEirHj3sSegVw8MZzgfKAzrT9CTr0aA==",
+ "version": "1.0.71",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.71.tgz",
+ "integrity": "sha512-Md9yEg406OBVBx3w4PeEj62TubulVLBcHleqmCoOoUmPgUxPZotUbrqz3rtbzADbXfrrD7JWvVsbd2UiNL194w==",
"cpu": [
"x64"
],
@@ -558,9 +558,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
- "version": "1.0.71-2",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.71-2.tgz",
- "integrity": "sha512-xKKk5o+zFJZ3EcoDOiCOdn1sbc8N/tHkn2j2sFQahE2SDp18ZpA2lzZp6XZ32VgxQJx0qlhPWh5BRn32Sc9csw==",
+ "version": "1.0.71",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.71.tgz",
+ "integrity": "sha512-ykLJYOqBj3jRB5IJCDugLClAqbr7DmtTbUjlNY7+Jdq/n6i+d7xUQGclf1IWL5gnxbGQVAf+zkToD+sRM389Kg==",
"cpu": [
"arm64"
],
@@ -575,9 +575,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
- "version": "1.0.71-2",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.71-2.tgz",
- "integrity": "sha512-We/kZNlEAXxhm9vMBae63Yy07RXUsTlOLsg5WrG7aNm0kh5ocUFpf5EodvtBbTmVIlGCvvm/RM1cYic6lPtD+w==",
+ "version": "1.0.71",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.71.tgz",
+ "integrity": "sha512-pC0FNHG+BBwZd6yZlM85kkAGN+uJhM6o+THi76N2GnnSxmw7+remb1mvYxdgRVbdCm+LBUIbCKRWJLuMwrfb6A==",
"cpu": [
"x64"
],
@@ -592,9 +592,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-arm64": {
- "version": "1.0.71-2",
- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.71-2.tgz",
- "integrity": "sha512-Hnyz/C4uNAXZVt6/RMtQBI89W0fxvUizm0mlp/ZohSthN03fv/PppbEsY7U5WqbTuDBuOKIU4kf3fCDC2elMCQ==",
+ "version": "1.0.71",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.71.tgz",
+ "integrity": "sha512-hBmDljFTjacxqZTasCEy43H8EIzuXB/hHEBBCMFjhB9J00nIxsO6Dh0woTifKpx7knTYZdpTjjca3D0pAoZlUA==",
"cpu": [
"arm64"
],
@@ -609,9 +609,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-x64": {
- "version": "1.0.71-2",
- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.71-2.tgz",
- "integrity": "sha512-CQDeUqq2wbM+a/Tec68O+6Gp8f3cUZ5DLAqcNwxzN+86b5Gsu9xyGMV2eIF/sEYxakhY4mpHhjwdZBySue7IBA==",
+ "version": "1.0.71",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.71.tgz",
+ "integrity": "sha512-CfTXU8pa5dxRz22xQzoi3TiG1PJo9+WR8PRDiPSdkIBSyPJ1NvX87DJmfXjTgeAfR+wkjt/p0keDCaBBVhNmUA==",
"cpu": [
"x64"
],
@@ -626,9 +626,9 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
- "version": "1.0.71-2",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.71-2.tgz",
- "integrity": "sha512-lb81urkKJ+yWIy62yw9NmyjX5eFiw0y2TVIkVje26ot+gCiCJPisqyRWwhkfq0g/YKRbS8uoX4r+KHAhRH1wlg==",
+ "version": "1.0.71",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.71.tgz",
+ "integrity": "sha512-+HI1DokixXhHUahj06Fw67ZAigBuXKC58BFma4UJOGrQsDgwOSbqeTQHCw6vuymzjKlg3sactfsCUTaefkjscQ==",
"cpu": [
"arm64"
],
@@ -643,9 +643,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
- "version": "1.0.71-2",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.71-2.tgz",
- "integrity": "sha512-j3mQO2OUVoO+ZGE5KGF3iiekTikQZDQTnZO9RquWPXLwQs6ZdgRw9pPhbpXh0eoM1osZAW1/tmafcyYNpdBgWA==",
+ "version": "1.0.71",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.71.tgz",
+ "integrity": "sha512-02kXOBd9CwBbCaztuf71WYWn+uGapCuiaasomN4tcMH3HBVZ4gi3J0ZUoRcgcS80xh81uQyeBHbnUKzb/RE/9A==",
"cpu": [
"x64"
],
diff --git a/test/harness/package.json b/test/harness/package.json
index bfc8879147..18b19e21ac 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.71-2",
+ "@github/copilot": "^1.0.71",
"@modelcontextprotocol/sdk": "^1.26.0",
"@types/node": "^25.3.3",
"@types/node-forge": "^1.3.14",
From bc82988d3e231eafb2818423a6ff721be2a451b6 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Thu, 16 Jul 2026 10:14:57 +0000
Subject: [PATCH 2/6] Route Python hooks.invoke through generated client-global
handler
CLI 1.0.71 promoted hooks.invoke to a client-global RPC method with a
generated HooksHandler interface. The handwritten SDK still registered its
own hooks.invoke handler, which only avoided colliding with the generated
one because global handlers were skipped when no LLM/telemetry adapter was
set.
Make the wiring intentional: add _HooksAdapter implementing the generated
HooksHandler protocol (routing HookInvokeRequest.sessionId to the matching
session's dispatcher), always register the client-global handlers with the
hooks adapter, and remove the redundant handwritten hooks.invoke
registrations and dead client-level handler.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ae226b7f-caf9-46f3-b8c5-b9a21c5d7951
---
python/copilot/client.py | 58 ++++++++++++++++++----------------------
python/test_client.py | 27 ++++++++++++++++---
2 files changed, 50 insertions(+), 35 deletions(-)
diff --git a/python/copilot/client.py b/python/copilot/client.py
index a56748478d..bb0d486d8b 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -73,6 +73,8 @@
RemoteSessionMode,
ServerRpc,
_ConnectResult,
+ _HookInvokeRequest,
+ _HookInvokeResponse,
from_datetime,
register_client_global_api_handlers,
register_client_session_api_handlers,
@@ -479,6 +481,25 @@ async def event(self, params: GitHubTelemetryNotification) -> None:
logger.warning("Error handling gitHubTelemetry.event notification", exc_info=True)
+class _HooksAdapter:
+ """Adapts session-scoped hook dispatch to the generated ``HooksHandler`` protocol.
+
+ ``hooks.invoke`` is a client-global RPC method whose payload carries a
+ ``sessionId``. This adapter routes each invocation to the matching session's
+ registered hook handlers.
+ """
+
+ def __init__(self, get_session: Callable[[str], CopilotSession | None]) -> None:
+ self._get_session = get_session
+
+ async def invoke(self, params: _HookInvokeRequest) -> _HookInvokeResponse:
+ session = self._get_session(params.session_id)
+ if session is None:
+ raise ValueError(f"unknown session {params.session_id}")
+ output = await session._handle_hooks_invoke(params.hook_type.value, params.input)
+ return _HookInvokeResponse(output=output)
+
+
@dataclass
class _CopilotClientOptions:
"""Internal configuration carrier used by :class:`CopilotClient`.
@@ -4072,7 +4093,6 @@ def handle_notification(method: str, params: dict):
self._client.set_request_handler(
"autoModeSwitch.request", self._handle_auto_mode_switch_request
)
- self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke)
self._client.set_request_handler(
"systemMessage.transform", self._handle_system_message_transform
)
@@ -4192,7 +4212,6 @@ def handle_notification(method: str, params: dict):
self._client.set_request_handler(
"autoModeSwitch.request", self._handle_auto_mode_switch_request
)
- self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke)
self._client.set_request_handler(
"systemMessage.transform", self._handle_system_message_transform
)
@@ -4282,16 +4301,19 @@ def _register_client_global_handlers(self) -> None:
github_telemetry_adapter = None
if self._on_github_telemetry is not None:
github_telemetry_adapter = _GitHubTelemetryAdapter(self._on_github_telemetry)
- if llm_inference_adapter is None and github_telemetry_adapter is None:
- return
register_client_global_api_handlers(
self._client,
ClientGlobalApiHandlers(
+ hooks=_HooksAdapter(self._get_session),
llm_inference=llm_inference_adapter,
git_hub_telemetry=github_telemetry_adapter,
),
)
+ def _get_session(self, session_id: str) -> CopilotSession | None:
+ with self._sessions_lock:
+ return self._sessions.get(session_id)
+
async def _set_llm_inference_provider(self) -> None:
if self._request_handler is None or self._rpc is None:
return
@@ -4364,34 +4386,6 @@ async def _handle_auto_mode_switch_request(self, params: dict) -> dict:
response = await session._handle_auto_mode_switch_request(params)
return {"response": response}
- async def _handle_hooks_invoke(self, params: dict) -> dict:
- """
- Handle a hooks invocation from the CLI server.
-
- Args:
- params: The hooks invocation parameters from the server.
-
- Returns:
- A dict containing the hook output.
-
- Raises:
- ValueError: If the request payload is invalid.
- """
- session_id = params.get("sessionId")
- hook_type = params.get("hookType")
- input_data = params.get("input")
-
- if not session_id or not hook_type:
- raise ValueError("invalid hooks invoke payload")
-
- with self._sessions_lock:
- session = self._sessions.get(session_id)
- if not session:
- raise ValueError(f"unknown session {session_id}")
-
- output = await session._handle_hooks_invoke(hook_type, input_data)
- return {"output": output}
-
async def _handle_system_message_transform(self, params: dict) -> dict:
"""Handle a systemMessage.transform request from the CLI server."""
session_id = params.get("sessionId")
diff --git a/python/test_client.py b/python/test_client.py
index aac334cd4a..66941f289d 100644
--- a/python/test_client.py
+++ b/python/test_client.py
@@ -2616,12 +2616,33 @@ async def on_telemetry(notification):
await client.force_stop()
@pytest.mark.asyncio
- async def test_event_handler_not_registered_without_option(self):
+ async def test_event_not_forwarded_without_option(self):
+ # Client-global handlers are always registered (so that hooks.invoke works),
+ # but without the on_github_telemetry option the telemetry adapter is inert:
+ # incoming events must not be forwarded to any callback.
client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
await client.start()
try:
- assert "gitHubTelemetry.event" not in client._client.notification_method_handlers
- assert "gitHubTelemetry.event" not in client._client.request_handlers
+ assert client._on_github_telemetry is None
+
+ # Dispatching a telemetry event is a harmless no-op when not opted in.
+ client._client._handle_message(
+ {
+ "jsonrpc": "2.0",
+ "method": "gitHubTelemetry.event",
+ "params": {
+ "sessionId": "sess-no-telemetry",
+ "restricted": False,
+ "event": {
+ "kind": "tool_call_executed",
+ "metrics": {"duration_ms": 1.0},
+ "properties": {"tool": "shell"},
+ "session_id": "sess-no-telemetry",
+ },
+ },
+ }
+ )
+ await asyncio.sleep(0)
finally:
await client.force_stop()
From c6cc3266a947f453accbdf83a7e1b0c160119288 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Thu, 16 Jul 2026 10:26:59 +0000
Subject: [PATCH 3/6] Route Node hooks.invoke through generated client-global
handler
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CLI 1.0.71 promoted hooks.invoke to a client-global RPC method with a
generated HooksHandler interface. The handwritten SDK registered its own
hooks.invoke handler on the connection, which the generated
registerClientGlobalApiHandlers then shadowed with an unwired handler that
threw "No hooks client-global handler registered" — so hooks never fired.
Wire the existing handleHooksInvoke routing into the generated
clientGlobalHandlers.hooks slot and drop the redundant handwritten
connection.onRequest("hooks.invoke") registration. Behavior is unchanged;
the dispatcher and its payload validation are reused as-is.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ae226b7f-caf9-46f3-b8c5-b9a21c5d7951
---
nodejs/src/client.ts | 14 +++++---------
nodejs/test/client.test.ts | 4 ++--
2 files changed, 7 insertions(+), 11 deletions(-)
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index 002836ad33..61f4a99416 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -830,6 +830,11 @@ export class CopilotClient {
private setupClientGlobalHandlers(): void {
const handlers: import("./generated/rpc.js").ClientGlobalApiHandlers = {};
+ // `hooks.invoke` is a client-global RPC method whose payload carries a
+ // `sessionId`; route each invocation to the matching session's dispatcher.
+ handlers.hooks = {
+ invoke: async (params) => await this.handleHooksInvoke(params),
+ };
if (this.requestHandler) {
handlers.llmInference = createCopilotRequestAdapter(this.requestHandler, () => {
if (!this.connection) {
@@ -2796,15 +2801,6 @@ export class CopilotClient {
await this.handleAutoModeSwitchRequest(params)
);
- this.connection.onRequest(
- "hooks.invoke",
- async (params: {
- sessionId: string;
- hookType: string;
- input: unknown;
- }): Promise<{ output?: unknown }> => await this.handleHooksInvoke(params)
- );
-
this.connection.onRequest(
"systemMessage.transform",
async (params: {
diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts
index 2d93e9e075..2585542b4b 100644
--- a/nodejs/test/client.test.ts
+++ b/nodejs/test/client.test.ts
@@ -3133,7 +3133,7 @@ describe("CopilotClient", () => {
it("routes hooks.invoke JSON-RPC requests to the SessionHooks handler", async () => {
// Validates the full JSON-RPC entry point used by the CLI:
- // CopilotClient.handleHooksInvoke({sessionId, hookType, input})
+ // clientGlobalHandlers.hooks.invoke({sessionId, hookType, input})
// → CopilotSession._handleHooksInvoke(hookType, input)
// → SessionHooks.onPostToolUseFailure(normalizedInput, {sessionId})
//
@@ -3164,7 +3164,7 @@ describe("CopilotClient", () => {
cwd: "/tmp",
};
- const response = await (client as any).handleHooksInvoke({
+ const response = await (client as any).clientGlobalHandlers.hooks.invoke({
sessionId: session.sessionId,
hookType: "postToolUseFailure",
input: failureInput,
From 8062497941ddf5695ace5f1cdf41c8da24f2b2e6 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Thu, 16 Jul 2026 10:44:12 +0000
Subject: [PATCH 4/6] Route Go hooks.invoke through generated client-global
handler
CLI 1.0.71 promoted hooks.invoke to a client-global RPC method whose
generated registration installs a hooks.invoke handler that rejects all
invocations unless the Hooks slot is populated. Whenever an LLM inference
or telemetry adapter was configured, that generated handler overrode the
handwritten hooks.invoke registration and hooks stopped firing (e.g. the
sub-agent hook test).
Always register the client-global handlers with a hooksAdapter that
delegates to the existing per-session dispatcher, and drop the redundant
handwritten hooks.invoke registration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ae226b7f-caf9-46f3-b8c5-b9a21c5d7951
---
go/client.go | 64 +++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 48 insertions(+), 16 deletions(-)
diff --git a/go/client.go b/go/client.go
index fa7159de74..f243268aa4 100644
--- a/go/client.go
+++ b/go/client.go
@@ -2290,7 +2290,6 @@ func (c *Client) setupNotificationHandler() {
c.client.SetRequestHandler("userInput.request", jsonrpc2.RequestHandlerFor(c.handleUserInputRequest))
c.client.SetRequestHandler("exitPlanMode.request", jsonrpc2.RequestHandlerFor(c.handleExitPlanModeRequest))
c.client.SetRequestHandler("autoModeSwitch.request", jsonrpc2.RequestHandlerFor(c.handleAutoModeSwitchRequest))
- c.client.SetRequestHandler("hooks.invoke", jsonrpc2.RequestHandlerFor(c.handleHooksInvoke))
c.client.SetRequestHandler("systemMessage.transform", jsonrpc2.RequestHandlerFor(c.handleSystemMessageTransform))
rpc.RegisterClientSessionAPIHandlers(c.client, func(sessionID string) *rpc.ClientSessionAPIHandlers {
c.sessionsMux.Lock()
@@ -2301,21 +2300,25 @@ func (c *Client) setupNotificationHandler() {
}
return session.clientSessionAPIs
})
- if c.options.RequestHandler != nil || c.options.OnGitHubTelemetry != nil {
- handlers := &rpc.ClientGlobalAPIHandlers{}
- if c.options.RequestHandler != nil {
- handlers.LlmInference = newCopilotRequestAdapter(c.options.RequestHandler, func() *rpc.ServerLlmInferenceAPI {
- if c.RPC == nil {
- return nil
- }
- return c.RPC.LlmInference
- })
- }
- if c.options.OnGitHubTelemetry != nil {
- handlers.GitHubTelemetry = &gitHubTelemetryAdapter{callback: c.options.OnGitHubTelemetry}
- }
- rpc.RegisterClientGlobalAPIHandlers(c.client, handlers)
+ // hooks.invoke is a client-global RPC method: one connection-level handler
+ // receives every hook callback and routes to the owning session via the
+ // payload's sessionId. Always register the global handlers so the generated
+ // hooks.invoke handler is wired to our dispatcher.
+ handlers := &rpc.ClientGlobalAPIHandlers{
+ Hooks: &hooksAdapter{client: c},
+ }
+ if c.options.RequestHandler != nil {
+ handlers.LlmInference = newCopilotRequestAdapter(c.options.RequestHandler, func() *rpc.ServerLlmInferenceAPI {
+ if c.RPC == nil {
+ return nil
+ }
+ return c.RPC.LlmInference
+ })
}
+ if c.options.OnGitHubTelemetry != nil {
+ handlers.GitHubTelemetry = &gitHubTelemetryAdapter{callback: c.options.OnGitHubTelemetry}
+ }
+ rpc.RegisterClientGlobalAPIHandlers(c.client, handlers)
}
// gitHubTelemetryAdapter adapts the OnGitHubTelemetry option to the generated
@@ -2423,7 +2426,8 @@ func (c *Client) handleAutoModeSwitchRequest(req autoModeSwitchRequest) (*autoMo
return &autoModeSwitchResponse{Response: response}, nil
}
-// handleHooksInvoke handles a hooks invocation from the CLI server.
+// handleHooksInvoke routes a hook callback to its owning session, keyed by the
+// payload's sessionId.
func (c *Client) handleHooksInvoke(req hooksInvokeRequest) (map[string]any, *jsonrpc2.Error) {
if req.SessionID == "" || req.Type == "" {
return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid hooks invoke payload"}
@@ -2448,6 +2452,34 @@ func (c *Client) handleHooksInvoke(req hooksInvokeRequest) (map[string]any, *jso
return result, nil
}
+// hooksAdapter implements the generated rpc.HooksHandler, delegating to the
+// client's per-session hook dispatcher.
+type hooksAdapter struct {
+ client *Client
+}
+
+func (a *hooksAdapter) Invoke(request *rpc.HookInvokeRequest) (*rpc.HookInvokeResponse, error) {
+ rawInput, err := json.Marshal(request.Input)
+ if err != nil {
+ return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("invalid hooks invoke payload: %v", err)}
+ }
+
+ result, rpcErr := a.client.handleHooksInvoke(hooksInvokeRequest{
+ SessionID: request.SessionID,
+ Type: string(request.HookType),
+ Input: rawInput,
+ })
+ if rpcErr != nil {
+ return nil, rpcErr
+ }
+
+ response := &rpc.HookInvokeResponse{}
+ if result != nil {
+ response.Output = result["output"]
+ }
+ return response, nil
+}
+
// handleSystemMessageTransform handles a system message transform request from the CLI server.
func (c *Client) handleSystemMessageTransform(req systemMessageTransformRequest) (systemMessageTransformResponse, *jsonrpc2.Error) {
if req.SessionID == "" {
From 6e748a56df7a58d038db6bb2deeedbe1e591efe6 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Thu, 16 Jul 2026 12:33:19 +0000
Subject: [PATCH 5/6] Fix Rust hook input deserialization for float timestamps
The Copilot CLI serializes hook input `timestamp` as a JSON float
(e.g. `1784203878038.0`). Rust's hand-authored hook input structs typed
`timestamp` as `i64`, so `serde_json::from_value` rejected the float,
`dispatch_hook` returned an error, and the session handler fell back to
an empty `{ "output": {} }` response. Hooks therefore never fired: e.g. a
preToolUse deny was dropped, the CLI executed the tool, and the replayed
conversation diverged ("No cached response" -> 500).
Other SDKs tolerate this incidentally (Go decodes `input` into `any` and
re-marshals, dropping the `.0`); Rust decodes strictly. Type the hook
input `timestamp` fields as `f64` to match the shape the runtime sends.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ae226b7f-caf9-46f3-b8c5-b9a21c5d7951
---
rust/src/hooks.rs | 32 ++++++++++++------------
rust/tests/e2e/hooks_extended.rs | 10 ++++----
rust/tests/e2e/pre_mcp_tool_call_hook.rs | 2 +-
3 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/rust/src/hooks.rs b/rust/src/hooks.rs
index ec8cdfa3a3..0c3d64076f 100644
--- a/rust/src/hooks.rs
+++ b/rust/src/hooks.rs
@@ -27,8 +27,8 @@ pub struct HookContext {
pub struct PreToolUseInput {
/// The runtime session ID of the session that triggered the hook.
pub session_id: String,
- /// Unix timestamp (ms).
- pub timestamp: i64,
+ /// Unix timestamp in ms (the runtime serializes this as a JSON float).
+ pub timestamp: f64,
/// Working directory.
#[serde(rename = "cwd")]
pub working_directory: PathBuf,
@@ -65,8 +65,8 @@ pub struct PreToolUseOutput {
pub struct PreMcpToolCallInput {
/// The runtime session ID of the session that triggered the hook.
pub session_id: String,
- /// Unix timestamp (ms).
- pub timestamp: i64,
+ /// Unix timestamp in ms (the runtime serializes this as a JSON float).
+ pub timestamp: f64,
/// Working directory.
#[serde(rename = "cwd")]
pub working_directory: PathBuf,
@@ -104,8 +104,8 @@ pub struct PreMcpToolCallOutput {
pub struct PostToolUseInput {
/// The runtime session ID of the session that triggered the hook.
pub session_id: String,
- /// Unix timestamp (ms).
- pub timestamp: i64,
+ /// Unix timestamp in ms (the runtime serializes this as a JSON float).
+ pub timestamp: f64,
/// Working directory.
#[serde(rename = "cwd")]
pub working_directory: PathBuf,
@@ -144,8 +144,8 @@ pub struct PostToolUseOutput {
pub struct PostToolUseFailureInput {
/// The runtime session ID of the session that triggered the hook.
pub session_id: String,
- /// Unix timestamp (ms).
- pub timestamp: i64,
+ /// Unix timestamp in ms (the runtime serializes this as a JSON float).
+ pub timestamp: f64,
/// Working directory.
#[serde(rename = "cwd")]
pub working_directory: PathBuf,
@@ -175,8 +175,8 @@ pub struct PostToolUseFailureOutput {
pub struct UserPromptSubmittedInput {
/// The runtime session ID of the session that triggered the hook.
pub session_id: String,
- /// Unix timestamp (ms).
- pub timestamp: i64,
+ /// Unix timestamp in ms (the runtime serializes this as a JSON float).
+ pub timestamp: f64,
/// Working directory.
#[serde(rename = "cwd")]
pub working_directory: PathBuf,
@@ -205,8 +205,8 @@ pub struct UserPromptSubmittedOutput {
pub struct SessionStartInput {
/// The runtime session ID of the session that triggered the hook.
pub session_id: String,
- /// Unix timestamp (ms).
- pub timestamp: i64,
+ /// Unix timestamp in ms (the runtime serializes this as a JSON float).
+ pub timestamp: f64,
/// Working directory.
#[serde(rename = "cwd")]
pub working_directory: PathBuf,
@@ -235,8 +235,8 @@ pub struct SessionStartOutput {
pub struct SessionEndInput {
/// The runtime session ID of the session that triggered the hook.
pub session_id: String,
- /// Unix timestamp (ms).
- pub timestamp: i64,
+ /// Unix timestamp in ms (the runtime serializes this as a JSON float).
+ pub timestamp: f64,
/// Working directory.
#[serde(rename = "cwd")]
pub working_directory: PathBuf,
@@ -271,8 +271,8 @@ pub struct SessionEndOutput {
pub struct ErrorOccurredInput {
/// The runtime session ID of the session that triggered the hook.
pub session_id: String,
- /// Unix timestamp (ms).
- pub timestamp: i64,
+ /// Unix timestamp in ms (the runtime serializes this as a JSON float).
+ pub timestamp: f64,
/// Working directory.
#[serde(rename = "cwd")]
pub working_directory: PathBuf,
diff --git a/rust/tests/e2e/hooks_extended.rs b/rust/tests/e2e/hooks_extended.rs
index 259d462d56..ab93a0c3cd 100644
--- a/rust/tests/e2e/hooks_extended.rs
+++ b/rust/tests/e2e/hooks_extended.rs
@@ -36,7 +36,7 @@ async fn should_invoke_onsessionstart_hook_on_new_session() {
session.send_and_wait("Say hi").await.expect("send");
let input = recv_with_timeout(&mut rx, "sessionStart hook").await;
assert_eq!(input.source, "new");
- assert!(input.timestamp > 0);
+ assert!(input.timestamp > 0.0);
assert!(!input.working_directory.as_os_str().is_empty());
session.disconnect().await.expect("disconnect session");
@@ -68,7 +68,7 @@ async fn should_invoke_onuserpromptsubmitted_hook_when_sending_a_message() {
session.send_and_wait("Say hello").await.expect("send");
let input = recv_with_timeout(&mut rx, "userPromptSubmitted hook").await;
assert!(input.prompt.contains("Say hello"));
- assert!(input.timestamp > 0);
+ assert!(input.timestamp > 0.0);
assert!(!input.working_directory.as_os_str().is_empty());
session.disconnect().await.expect("disconnect session");
@@ -100,7 +100,7 @@ async fn should_invoke_onsessionend_hook_when_session_is_disconnected() {
session.send_and_wait("Say hi").await.expect("send");
session.disconnect().await.expect("disconnect session");
let input = recv_with_timeout(&mut rx, "sessionEnd hook").await;
- assert!(input.timestamp > 0);
+ assert!(input.timestamp > 0.0);
assert!(!input.working_directory.as_os_str().is_empty());
client.stop().await.expect("stop client");
@@ -237,7 +237,7 @@ async fn should_invoke_sessionend_hook() {
session.send_and_wait("Say bye").await.expect("send");
session.disconnect().await.expect("disconnect session");
let input = recv_with_timeout(&mut rx, "sessionEnd hook").await;
- assert!(input.timestamp > 0);
+ assert!(input.timestamp > 0.0);
client.stop().await.expect("stop client");
})
@@ -412,7 +412,7 @@ async fn should_invoke_posttoolusefailure_hook_for_failed_tool_result() {
.as_str()
.is_some_and(|path| path.contains("missing.txt"))
);
- assert!(input.timestamp > 0);
+ assert!(input.timestamp > 0.0);
assert!(!input.working_directory.as_os_str().is_empty());
assert!(
assistant_message_content(&answer).contains("HOOK_FAILURE_GUIDANCE_APPLIED")
diff --git a/rust/tests/e2e/pre_mcp_tool_call_hook.rs b/rust/tests/e2e/pre_mcp_tool_call_hook.rs
index 5dc782c963..fd05796fcd 100644
--- a/rust/tests/e2e/pre_mcp_tool_call_hook.rs
+++ b/rust/tests/e2e/pre_mcp_tool_call_hook.rs
@@ -126,7 +126,7 @@ async fn should_set_meta_via_premcptoolcall_hook() {
assert_eq!(input.server_name, "meta-echo");
assert_eq!(input.tool_name, "echo_meta");
assert!(!input.working_directory.as_os_str().is_empty());
- assert!(input.timestamp > 0);
+ assert!(input.timestamp > 0.0);
session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
From 1e119c1168c7264590770bd23b75156f05b782e6 Mon Sep 17 00:00:00 2001
From: Stephen Toub
Date: Thu, 16 Jul 2026 09:20:49 -0400
Subject: [PATCH 6/6] Fix .NET build break and float hook timestamps for
hooks.invoke
CLI 1.0.71 promoted hooks.invoke to an internal client-global RPC method.
The C# codegen still emitted its internal request/result DTOs behind a public
IHooksHandler surface, producing CS0050/CS0051 inconsistent-accessibility
errors that broke the entire .NET build. It also registered a second, unwired
hooks.invoke handler that would shadow the working handwritten one.
Filter internal client-global and client-session methods in the C# code
generator so no generated interface, handler property, or RPC registration is
emitted for internal methods like hooks.invoke. The handwritten
SetLocalRpcMethod(hooks.invoke, ...) registration continues to serve hooks.
This mirrors, for .NET's static typing, the routing fixes already applied to
Node, Python, and Go.
Also tolerate hook timestamp epoch milliseconds encoded as either JSON
integers or floats in UnixMillisecondsDateTimeOffsetConverter, covering the
CLI 1.0.71 float serialization (matching the Rust fix).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 94a20428-6b0e-4733-a354-0abf2d186320
---
dotnet/src/Generated/Rpc.cs | 152 ------------------
...UnixMillisecondsDateTimeOffsetConverter.cs | 10 +-
scripts/codegen/csharp.ts | 17 +-
3 files changed, 23 insertions(+), 156 deletions(-)
diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs
index 67d72203f5..bf73bdfaf7 100644
--- a/dotnet/src/Generated/Rpc.cs
+++ b/dotnet/src/Generated/Rpc.cs
@@ -12444,32 +12444,6 @@ public sealed class CanvasProviderInvokeActionRequest
public string SessionId { get; set; } = string.Empty;
}
-/// Optional output returned by an SDK callback hook.
-[Experimental(Diagnostics.Experimental)]
-internal sealed class HookInvokeResponse
-{
- /// Gets or sets the output value.
- [JsonPropertyName("output")]
- public JsonElement? Output { get; set; }
-}
-
-/// Runtime-owned wire payload for a server-to-client hook callback invocation.
-[Experimental(Diagnostics.Experimental)]
-internal sealed class HookInvokeRequest
-{
- /// Gets or sets the hookType value.
- [JsonPropertyName("hookType")]
- public HookType HookType { get; set; }
-
- /// Gets or sets the input value.
- [JsonPropertyName("input")]
- public JsonElement Input { get; set; }
-
- /// Gets or sets the sessionId value.
- [JsonPropertyName("sessionId")]
- public string SessionId { get; set; } = string.Empty;
-}
-
/// Acknowledgement. Returning successfully simply means the SDK accepted the start frame; it does not imply the request will succeed.
[Experimental(Diagnostics.Experimental)]
public sealed class LlmInferenceHttpRequestStartResult
@@ -19055,111 +19029,6 @@ public override void Write(Utf8JsonWriter writer, SessionFsSqliteQueryType value
}
-/// Hook event name dispatched through the SDK callback transport.
-[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 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));
- }
- }
-}
-
-
/// Transport the runtime would otherwise use for this request. `http` (the default when absent) covers plain HTTP and SSE responses; `websocket` indicates a full-duplex message channel where each body chunk maps to one WebSocket message and the `binary` flag distinguishes text from binary frames. The SDK consumer uses this to decide whether to service the request with an HTTP client or a WebSocket client. It is the one piece of request metadata the consumer cannot reliably infer from the URL or headers alone.
[Experimental(Diagnostics.Experimental)]
[JsonConverter(typeof(Converter))]
@@ -23831,17 +23700,6 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, FuncHandles `hooks` client global API methods.
-[Experimental(Diagnostics.Experimental)]
-public interface IHooksHandler
-{
- /// Dispatches one SDK callback hook from the runtime to the connection that registered it. Internal transport plumbing: clients opt in through session initialization and the Rust hook processor owns ordering, policy, timeout, and callback routing.
- /// Runtime-owned wire payload for a server-to-client hook callback invocation.
- /// The to monitor for cancellation requests. The default is .
- /// Optional output returned by an SDK callback hook.
- Task InvokeAsync(HookInvokeRequest request, CancellationToken cancellationToken = default);
-}
-
/// Handles `llmInference` client global API methods.
[Experimental(Diagnostics.Experimental)]
public interface ILlmInferenceHandler
@@ -23871,9 +23729,6 @@ public interface IGitHubTelemetryHandler
/// Provides all client global API handler groups for a connection.
public sealed class ClientGlobalApiHandlers
{
- /// Optional handler for Hooks client global API methods.
- public IHooksHandler? Hooks { get; set; }
-
/// Optional handler for LlmInference client global API methods.
public ILlmInferenceHandler? LlmInference { get; set; }
@@ -23892,11 +23747,6 @@ internal static class ClientGlobalApiRegistration
///
public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiHandlers handlers)
{
- rpc.SetLocalRpcMethod("hooks.invoke", (Func>)(async (request, cancellationToken) =>
- {
- var handler = handlers.Hooks ?? throw new InvalidOperationException("No hooks client-global handler registered");
- return await handler.InvokeAsync(request, cancellationToken);
- }), singleObjectParam: true);
rpc.SetLocalRpcMethod("llmInference.httpRequestStart", (Func>)(async (request, cancellationToken) =>
{
var handler = handlers.LlmInference ?? throw new InvalidOperationException("No llmInference client-global handler registered");
@@ -24354,8 +24204,6 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(HistorySummarizeForHandoffResult))]
[JsonSerializable(typeof(HistoryTruncateRequest))]
[JsonSerializable(typeof(HistoryTruncateResult))]
-[JsonSerializable(typeof(HookInvokeRequest))]
-[JsonSerializable(typeof(HookInvokeResponse))]
[JsonSerializable(typeof(IDictionary))]
[JsonSerializable(typeof(IList))]
[JsonSerializable(typeof(InstalledPlugin))]
diff --git a/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs b/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs
index 4b8fcc3616..8e176fbafa 100644
--- a/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs
+++ b/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs
@@ -13,8 +13,14 @@ namespace GitHub.Copilot;
public sealed class UnixMillisecondsDateTimeOffsetConverter : JsonConverter
{
///
- public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
- DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64());
+ public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ // The CLI may serialize the epoch-millisecond timestamp as a JSON integer
+ // or as a floating-point number (e.g. 1700000000000.0). GetInt64 throws on a
+ // fractional token, so fall back to reading a double and truncating.
+ long milliseconds = reader.TryGetInt64(out long value) ? value : (long)reader.GetDouble();
+ return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds);
+ }
///
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts
index caa67e1682..24ec217f5b 100644
--- a/scripts/codegen/csharp.ts
+++ b/scripts/codegen/csharp.ts
@@ -27,6 +27,7 @@ import {
findSharedSchemaDefinitions,
postProcessSchema,
propagateInternalVisibility,
+ filterNodeByVisibility,
resolveRef,
resolveObjectSchema,
resolveSchema,
@@ -2490,11 +2491,23 @@ function generateRpcCode(
let sessionRpcParts: string[] = [];
if (schema.session) sessionRpcParts = emitSessionRpcClasses(schema.session, classes);
+ // Client handler surfaces (interfaces, handler properties, RPC registration)
+ // are only generated for public methods. Internal client methods (e.g.
+ // `hooks.invoke`) are runtime transport plumbing and must not surface any
+ // generated code — including their request/result DTOs, which would
+ // otherwise leak as `internal` types referenced by a `public` handler
+ // interface (CS0050/CS0051 inconsistent accessibility).
let clientSessionParts: string[] = [];
- if (schema.clientSession) clientSessionParts = emitClientSessionApiRegistration(schema.clientSession, classes);
+ if (schema.clientSession) {
+ const publicClientSession = filterNodeByVisibility(schema.clientSession, "public");
+ if (publicClientSession) clientSessionParts = emitClientSessionApiRegistration(publicClientSession, classes);
+ }
let clientGlobalParts: string[] = [];
- if (schema.clientGlobal) clientGlobalParts = emitClientGlobalApiRegistration(schema.clientGlobal, classes);
+ if (schema.clientGlobal) {
+ const publicClientGlobal = filterNodeByVisibility(schema.clientGlobal, "public");
+ if (publicClientGlobal) clientGlobalParts = emitClientGlobalApiRegistration(publicClientGlobal, classes);
+ }
const lines: string[] = [];
lines.push(`${COPYRIGHT}