From 0ba3665e9b25d86719403d150d53b08d6c6be7f2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 13 May 2026 12:28:42 -0400 Subject: [PATCH 1/6] Fix generated identifier sanitization Sanitize generated string enum identifiers across C#, Go, and Rust so schema values containing path or URL separators produce valid language identifiers while preserving wire values. Also emit top-level Rust discriminated unions needed by newer schemas and refresh the Rust generated output from the currently imported 1.0.46 schema. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 4 +- rust/src/generated/api_types.rs | 232 ++++++++++++++++---------------- scripts/codegen/csharp.ts | 39 +++++- scripts/codegen/go.ts | 9 +- scripts/codegen/rust.ts | 50 ++++++- 5 files changed, 201 insertions(+), 133 deletions(-) diff --git a/rust/README.md b/rust/README.md index 05177132d..cf9e4b839 100644 --- a/rust/README.md +++ b/rust/README.md @@ -166,8 +166,8 @@ let forked = client .rpc() .sessions() .fork(github_copilot_sdk::generated::api_types::SessionsForkRequest { - session_id: "session-id".to_string(), - from_message_id: None, + session_id: "session-id".into(), + to_event_id: None, }) .await?; ``` diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 35032e8c0..0fcaf1e2f 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -1064,157 +1064,157 @@ pub struct NameSetRequest { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalCommands { +pub struct PermissionDecisionApproveOnce { + /// The permission request was approved for this one instance + pub kind: PermissionDecisionApproveOnceKind, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionDecisionApproveForSessionApprovalCommands { pub command_identifiers: Vec, - pub kind: PermissionDecisionApproveForLocationApprovalCommandsKind, + pub kind: PermissionDecisionApproveForSessionApprovalCommandsKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalRead { - pub kind: PermissionDecisionApproveForLocationApprovalReadKind, +pub struct PermissionDecisionApproveForSessionApprovalRead { + pub kind: PermissionDecisionApproveForSessionApprovalReadKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalWrite { - pub kind: PermissionDecisionApproveForLocationApprovalWriteKind, +pub struct PermissionDecisionApproveForSessionApprovalWrite { + pub kind: PermissionDecisionApproveForSessionApprovalWriteKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalMcp { - pub kind: PermissionDecisionApproveForLocationApprovalMcpKind, +pub struct PermissionDecisionApproveForSessionApprovalMcp { + pub kind: PermissionDecisionApproveForSessionApprovalMcpKind, pub server_name: String, pub tool_name: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalMcpSampling { - pub kind: PermissionDecisionApproveForLocationApprovalMcpSamplingKind, +pub struct PermissionDecisionApproveForSessionApprovalMcpSampling { + pub kind: PermissionDecisionApproveForSessionApprovalMcpSamplingKind, pub server_name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalMemory { - pub kind: PermissionDecisionApproveForLocationApprovalMemoryKind, +pub struct PermissionDecisionApproveForSessionApprovalMemory { + pub kind: PermissionDecisionApproveForSessionApprovalMemoryKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalCustomTool { - pub kind: PermissionDecisionApproveForLocationApprovalCustomToolKind, +pub struct PermissionDecisionApproveForSessionApprovalCustomTool { + pub kind: PermissionDecisionApproveForSessionApprovalCustomToolKind, pub tool_name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalExtensionManagement { - pub kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind, +pub struct PermissionDecisionApproveForSessionApprovalExtensionManagement { + pub kind: PermissionDecisionApproveForSessionApprovalExtensionManagementKind, #[serde(skip_serializing_if = "Option::is_none")] pub operation: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess { +pub struct PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess { pub extension_name: String, - pub kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, + pub kind: PermissionDecisionApproveForSessionApprovalExtensionPermissionAccessKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForLocation { - /// The approval to persist for this location - pub approval: PermissionDecisionApproveForLocationApproval, - /// Approved and persisted for this project location - pub kind: PermissionDecisionApproveForLocationKind, - /// The location key (git root or cwd) to persist the approval to - pub location_key: String, +pub struct PermissionDecisionApproveForSession { + /// The approval to add as a session-scoped rule + #[serde(skip_serializing_if = "Option::is_none")] + pub approval: Option, + /// The URL domain to approve for this session + #[serde(skip_serializing_if = "Option::is_none")] + pub domain: Option, + /// Approved and remembered for the rest of the session + pub kind: PermissionDecisionApproveForSessionKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalCommands { +pub struct PermissionDecisionApproveForLocationApprovalCommands { pub command_identifiers: Vec, - pub kind: PermissionDecisionApproveForSessionApprovalCommandsKind, + pub kind: PermissionDecisionApproveForLocationApprovalCommandsKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalRead { - pub kind: PermissionDecisionApproveForSessionApprovalReadKind, +pub struct PermissionDecisionApproveForLocationApprovalRead { + pub kind: PermissionDecisionApproveForLocationApprovalReadKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalWrite { - pub kind: PermissionDecisionApproveForSessionApprovalWriteKind, +pub struct PermissionDecisionApproveForLocationApprovalWrite { + pub kind: PermissionDecisionApproveForLocationApprovalWriteKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalMcp { - pub kind: PermissionDecisionApproveForSessionApprovalMcpKind, +pub struct PermissionDecisionApproveForLocationApprovalMcp { + pub kind: PermissionDecisionApproveForLocationApprovalMcpKind, pub server_name: String, pub tool_name: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalMcpSampling { - pub kind: PermissionDecisionApproveForSessionApprovalMcpSamplingKind, +pub struct PermissionDecisionApproveForLocationApprovalMcpSampling { + pub kind: PermissionDecisionApproveForLocationApprovalMcpSamplingKind, pub server_name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalMemory { - pub kind: PermissionDecisionApproveForSessionApprovalMemoryKind, +pub struct PermissionDecisionApproveForLocationApprovalMemory { + pub kind: PermissionDecisionApproveForLocationApprovalMemoryKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalCustomTool { - pub kind: PermissionDecisionApproveForSessionApprovalCustomToolKind, +pub struct PermissionDecisionApproveForLocationApprovalCustomTool { + pub kind: PermissionDecisionApproveForLocationApprovalCustomToolKind, pub tool_name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalExtensionManagement { - pub kind: PermissionDecisionApproveForSessionApprovalExtensionManagementKind, +pub struct PermissionDecisionApproveForLocationApprovalExtensionManagement { + pub kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind, #[serde(skip_serializing_if = "Option::is_none")] pub operation: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess { +pub struct PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess { pub extension_name: String, - pub kind: PermissionDecisionApproveForSessionApprovalExtensionPermissionAccessKind, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveForSession { - /// The approval to add as a session-scoped rule - #[serde(skip_serializing_if = "Option::is_none")] - pub approval: Option, - /// The URL domain to approve for this session - #[serde(skip_serializing_if = "Option::is_none")] - pub domain: Option, - /// Approved and remembered for the rest of the session - pub kind: PermissionDecisionApproveForSessionKind, + pub kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionDecisionApproveOnce { - /// The permission request was approved for this one instance - pub kind: PermissionDecisionApproveOnceKind, +pub struct PermissionDecisionApproveForLocation { + /// The approval to persist for this location + pub approval: PermissionDecisionApproveForLocationApproval, + /// Approved and persisted for this project location + pub kind: PermissionDecisionApproveForLocationKind, + /// The location key (git root or cwd) to persist the approval to + pub location_key: String, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -3202,165 +3202,165 @@ pub enum SessionMode { Unknown, } +/// The permission request was approved for this one instance #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalCommandsKind { +pub enum PermissionDecisionApproveOnceKind { + #[serde(rename = "approve-once")] + ApproveOnce, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionDecisionApproveForSessionApprovalCommandsKind { #[serde(rename = "commands")] Commands, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalReadKind { +pub enum PermissionDecisionApproveForSessionApprovalReadKind { #[serde(rename = "read")] Read, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalWriteKind { +pub enum PermissionDecisionApproveForSessionApprovalWriteKind { #[serde(rename = "write")] Write, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalMcpKind { +pub enum PermissionDecisionApproveForSessionApprovalMcpKind { #[serde(rename = "mcp")] Mcp, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalMcpSamplingKind { +pub enum PermissionDecisionApproveForSessionApprovalMcpSamplingKind { #[serde(rename = "mcp-sampling")] McpSampling, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalMemoryKind { +pub enum PermissionDecisionApproveForSessionApprovalMemoryKind { #[serde(rename = "memory")] Memory, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalCustomToolKind { +pub enum PermissionDecisionApproveForSessionApprovalCustomToolKind { #[serde(rename = "custom-tool")] CustomTool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalExtensionManagementKind { +pub enum PermissionDecisionApproveForSessionApprovalExtensionManagementKind { #[serde(rename = "extension-management")] ExtensionManagement, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind { +pub enum PermissionDecisionApproveForSessionApprovalExtensionPermissionAccessKind { #[serde(rename = "extension-permission-access")] ExtensionPermissionAccess, } -/// The approval to persist for this location +/// The approval to add as a session-scoped rule #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] -pub enum PermissionDecisionApproveForLocationApproval { - Commands(PermissionDecisionApproveForLocationApprovalCommands), - Read(PermissionDecisionApproveForLocationApprovalRead), - Write(PermissionDecisionApproveForLocationApprovalWrite), - Mcp(PermissionDecisionApproveForLocationApprovalMcp), - McpSampling(PermissionDecisionApproveForLocationApprovalMcpSampling), - Memory(PermissionDecisionApproveForLocationApprovalMemory), - CustomTool(PermissionDecisionApproveForLocationApprovalCustomTool), - ExtensionManagement(PermissionDecisionApproveForLocationApprovalExtensionManagement), - ExtensionPermissionAccess( - PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess, - ), +pub enum PermissionDecisionApproveForSessionApproval { + Commands(PermissionDecisionApproveForSessionApprovalCommands), + Read(PermissionDecisionApproveForSessionApprovalRead), + Write(PermissionDecisionApproveForSessionApprovalWrite), + Mcp(PermissionDecisionApproveForSessionApprovalMcp), + McpSampling(PermissionDecisionApproveForSessionApprovalMcpSampling), + Memory(PermissionDecisionApproveForSessionApprovalMemory), + CustomTool(PermissionDecisionApproveForSessionApprovalCustomTool), + ExtensionManagement(PermissionDecisionApproveForSessionApprovalExtensionManagement), + ExtensionPermissionAccess(PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess), } -/// Approved and persisted for this project location +/// Approved and remembered for the rest of the session #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForLocationKind { - #[serde(rename = "approve-for-location")] - ApproveForLocation, +pub enum PermissionDecisionApproveForSessionKind { + #[serde(rename = "approve-for-session")] + ApproveForSession, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalCommandsKind { +pub enum PermissionDecisionApproveForLocationApprovalCommandsKind { #[serde(rename = "commands")] Commands, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalReadKind { +pub enum PermissionDecisionApproveForLocationApprovalReadKind { #[serde(rename = "read")] Read, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalWriteKind { +pub enum PermissionDecisionApproveForLocationApprovalWriteKind { #[serde(rename = "write")] Write, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalMcpKind { +pub enum PermissionDecisionApproveForLocationApprovalMcpKind { #[serde(rename = "mcp")] Mcp, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalMcpSamplingKind { +pub enum PermissionDecisionApproveForLocationApprovalMcpSamplingKind { #[serde(rename = "mcp-sampling")] McpSampling, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalMemoryKind { +pub enum PermissionDecisionApproveForLocationApprovalMemoryKind { #[serde(rename = "memory")] Memory, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalCustomToolKind { +pub enum PermissionDecisionApproveForLocationApprovalCustomToolKind { #[serde(rename = "custom-tool")] CustomTool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalExtensionManagementKind { +pub enum PermissionDecisionApproveForLocationApprovalExtensionManagementKind { #[serde(rename = "extension-management")] ExtensionManagement, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionApprovalExtensionPermissionAccessKind { +pub enum PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind { #[serde(rename = "extension-permission-access")] ExtensionPermissionAccess, } -/// The approval to add as a session-scoped rule +/// The approval to persist for this location #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] -pub enum PermissionDecisionApproveForSessionApproval { - Commands(PermissionDecisionApproveForSessionApprovalCommands), - Read(PermissionDecisionApproveForSessionApprovalRead), - Write(PermissionDecisionApproveForSessionApprovalWrite), - Mcp(PermissionDecisionApproveForSessionApprovalMcp), - McpSampling(PermissionDecisionApproveForSessionApprovalMcpSampling), - Memory(PermissionDecisionApproveForSessionApprovalMemory), - CustomTool(PermissionDecisionApproveForSessionApprovalCustomTool), - ExtensionManagement(PermissionDecisionApproveForSessionApprovalExtensionManagement), - ExtensionPermissionAccess(PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess), -} - -/// Approved and remembered for the rest of the session -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveForSessionKind { - #[serde(rename = "approve-for-session")] - ApproveForSession, +pub enum PermissionDecisionApproveForLocationApproval { + Commands(PermissionDecisionApproveForLocationApprovalCommands), + Read(PermissionDecisionApproveForLocationApprovalRead), + Write(PermissionDecisionApproveForLocationApprovalWrite), + Mcp(PermissionDecisionApproveForLocationApprovalMcp), + McpSampling(PermissionDecisionApproveForLocationApprovalMcpSampling), + Memory(PermissionDecisionApproveForLocationApprovalMemory), + CustomTool(PermissionDecisionApproveForLocationApprovalCustomTool), + ExtensionManagement(PermissionDecisionApproveForLocationApprovalExtensionManagement), + ExtensionPermissionAccess( + PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess, + ), } -/// The permission request was approved for this one instance +/// Approved and persisted for this project location #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionApproveOnceKind { - #[serde(rename = "approve-once")] - ApproveOnce, +pub enum PermissionDecisionApproveForLocationKind { + #[serde(rename = "approve-for-location")] + ApproveForLocation, } /// Approved and persisted across sessions diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index edfdd81b1..1ef68507c 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -118,18 +118,41 @@ function xmlDocEnumComment(description: string | undefined, indent: string): str } function toPascalCase(name: string): string { - if (name.includes("_") || name.includes("-")) { - return name.split(/[-_]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); - } + const parts = splitCSharpIdentifierParts(name); + if (parts.length > 1) return parts.map(toPascalCasePart).join(""); return name.charAt(0).toUpperCase() + name.slice(1); } function typeToClassName(typeName: string): string { - return typeName.split(/[._]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); + return splitCSharpIdentifierParts(typeName).map(toPascalCasePart).join(""); +} + +function splitCSharpIdentifierParts(value: string): string[] { + return value.split(/[^A-Za-z0-9]+/).filter(Boolean); +} + +function toPascalCasePart(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); } -function toPascalCaseEnumMember(value: string): string { - return value.split(/[-_.]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); +function toCSharpIdentifier(value: string, fallback: string): string { + let identifier = splitCSharpIdentifierParts(value).map(toPascalCasePart).join(""); + if (!identifier) { + identifier = fallback; + } else if (!/^[A-Za-z_]/.test(identifier)) { + identifier = `${fallback}${identifier}`; + } + return identifier; +} + +function uniqueCSharpIdentifier(value: string, used: Set, fallback: string): string { + const base = toCSharpIdentifier(value, fallback); + let identifier = base; + for (let i = 2; used.has(identifier); i++) { + identifier = `${base}${i}`; + } + used.add(identifier); + return identifier; } async function formatCSharpFile(filePath: string): Promise { @@ -374,9 +397,11 @@ function getOrCreateEnum( lines.push(` }`, ""); lines.push(` /// Gets the value associated with this .`); lines.push(` public string Value => _value ?? string.Empty;`, ""); + const usedMemberNames = new Set(); for (const value of values) { + const memberName = uniqueCSharpIdentifier(value, usedMemberNames, "Value"); lines.push(` /// Gets the ${escapeXml(value)} value.`); - lines.push(` public static ${enumName} ${toPascalCaseEnumMember(value)} { get; } = new("${value}");`, ""); + lines.push(` public static ${enumName} ${memberName} { get; } = new("${escapeCSharpStringLiteral(value)}");`, ""); } lines.push(` /// Returns a value indicating whether two instances are equivalent.`); lines.push(` public static bool operator ==(${enumName} left, ${enumName} right) => left.Equals(right);`, ""); diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 5779f2d3b..cfc9008bc 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -52,7 +52,8 @@ const wrapGoCommentText = wordwrap(goCommentTextWrapLength); function toPascalCase(s: string): string { return s - .split(/[._]/) + .split(/[^A-Za-z0-9]+/) + .filter((word) => word.length > 0) .map((w) => goInitialisms.has(w.toLowerCase()) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)) .join(""); } @@ -92,7 +93,7 @@ function splitGoIdentifierWords(name: string): string[] { return name .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") .replace(/([a-z0-9])([A-Z])/g, "$1_$2") - .split(/[._-]/) + .split(/[^A-Za-z0-9]+/) .filter((word) => word.length > 0); } @@ -558,14 +559,14 @@ function getOrCreateGoEnum( } function goEnumConstSuffix(value: string): string { - return value - .split(/[-_.]/) + const suffix = splitGoIdentifierWords(value) .map((word) => goInitialisms.has(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1) ) .join(""); + return suffix || "Value"; } function goDiscriminatedUnionVariantTypeName( diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 4300c70e4..1fed3d176 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -59,11 +59,38 @@ const STRING_NEWTYPE_OVERRIDES: Record = { function toPascalCase(s: string): string { return s - .split(/[._\-\s]+/) + .split(/[^A-Za-z0-9]+/) + .filter(Boolean) .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(""); } +function toRustPascalIdentifier(value: string, fallback: string): string { + let identifier = toPascalCase(value); + if (!identifier) { + identifier = fallback; + } else if (!/^[A-Za-z_]/.test(identifier)) { + identifier = `${fallback}${identifier}`; + } + + return RUST_KEYWORDS.has(identifier) ? `${identifier}Value` : identifier; +} + +function uniqueRustPascalIdentifier( + value: string, + used: Set, + fallback: string, + reserved: Set = new Set(), +): string { + const base = toRustPascalIdentifier(value, fallback); + let identifier = base; + for (let i = 2; used.has(identifier) || reserved.has(identifier); i++) { + identifier = `${base}${i}`; + } + used.add(identifier); + return identifier; +} + function toSnakeCase(s: string): string { return s .replace(/([A-Z])/g, "_$1") @@ -233,10 +260,16 @@ function tryEmitRustDiscriminatedUnion( lines.push("#[serde(untagged)]"); lines.push(`pub enum ${enumName} {`); + const usedVariantNames = new Set(); for (const { schema: variantSchema, typeName } of resolvedVariants) { const kind = ((variantSchema.properties?.kind as JSONSchema7 | undefined) ?.const ?? typeName) as string; - lines.push(` ${toPascalCase(kind)}(${stripOption(typeName)}),`); + const variantName = uniqueRustPascalIdentifier( + kind, + usedVariantNames, + "Variant", + ); + lines.push(` ${variantName}(${stripOption(typeName)}),`); } lines.push("}"); @@ -617,8 +650,15 @@ function emitRustStringEnum( lines.push("#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]"); lines.push(`pub enum ${enumName} {`); + const usedVariantNames = new Set(); + const reservedVariantNames = new Set(["Unknown"]); for (const value of values) { - const variantName = toPascalCase(value); + const variantName = uniqueRustPascalIdentifier( + value, + usedVariantNames, + "Value", + reservedVariantNames, + ); if (variantName !== value) { lines.push(` #[serde(rename = "${value}")]`); } @@ -651,7 +691,7 @@ function emitRustConstStringEnum( } lines.push("#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]"); lines.push(`pub enum ${enumName} {`); - const variantName = toPascalCase(value); + const variantName = toRustPascalIdentifier(value, "Value"); if (variantName !== value) { lines.push(` #[serde(rename = "${value}")]`); } @@ -927,6 +967,8 @@ function generateApiTypesCode(apiSchema: ApiSchema): string { schema.description, isSchemaExperimental(schema), ); + } else if (getUnionVariants(schema)) { + tryEmitRustDiscriminatedUnion(schema, name, "", ctx); } else if (isObjectSchema(schema)) { emitRustStruct(name, schema, ctx, schema.description); } From 831c6de9fbbed06cf5c75d0104324cc27b7e1915 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 13 May 2026 13:18:06 -0400 Subject: [PATCH 2/6] Harden enum collision handling Fail code generation when sanitized enum values collide with existing public member names instead of stabilizing arbitrary numeric suffixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/codegen/csharp.ts | 12 +++++++----- scripts/codegen/go.ts | 11 ++++++++++- scripts/codegen/rust.ts | 9 +++++---- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 1ef68507c..a1401e1a5 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -146,10 +146,11 @@ function toCSharpIdentifier(value: string, fallback: string): string { } function uniqueCSharpIdentifier(value: string, used: Set, fallback: string): string { - const base = toCSharpIdentifier(value, fallback); - let identifier = base; - for (let i = 2; used.has(identifier); i++) { - identifier = `${base}${i}`; + const identifier = toCSharpIdentifier(value, fallback); + if (used.has(identifier)) { + throw new Error( + `Generated C# string enum member identifier "${identifier}" is not unique for value "${value}". Add an explicit naming rule instead of stabilizing an arbitrary public member name.` + ); } used.add(identifier); return identifier; @@ -334,6 +335,7 @@ const COPYRIGHT = `/*----------------------------------------------------------- const EXPERIMENTAL_ATTRIBUTE = "[Experimental(Diagnostics.Experimental)]"; const OBSOLETE_ATTRIBUTE = `[Obsolete("This member is deprecated and will be removed in a future version.")]`; +const STRING_ENUM_RESERVED_MEMBER_NAMES = new Set(["Value", "Equals", "GetHashCode", "ToString", "Converter"]); function experimentalAttribute(indent = ""): string { return `${indent}${EXPERIMENTAL_ATTRIBUTE}`; @@ -397,7 +399,7 @@ function getOrCreateEnum( lines.push(` }`, ""); lines.push(` /// Gets the value associated with this .`); lines.push(` public string Value => _value ?? string.Empty;`, ""); - const usedMemberNames = new Set(); + const usedMemberNames = new Set(STRING_ENUM_RESERVED_MEMBER_NAMES); for (const value of values) { const memberName = uniqueCSharpIdentifier(value, usedMemberNames, "Value"); lines.push(` /// Gets the ${escapeXml(value)} value.`); diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index cfc9008bc..b1a8fb080 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -548,8 +548,17 @@ function getOrCreateGoEnum( const consts = values .map((value) => ({ value, constSuffix: goEnumConstSuffix(value) })) .sort((left, right) => `${enumName}${left.constSuffix}`.localeCompare(`${enumName}${right.constSuffix}`)); + const usedConstNames = new Map(); for (const { value, constSuffix } of consts) { - lines.push(`\t${enumName}${constSuffix} ${enumName} = "${value}"`); + const constName = `${enumName}${constSuffix}`; + const existingValue = usedConstNames.get(constName); + if (existingValue !== undefined) { + throw new Error( + `Generated Go enum const identifier "${constName}" is not unique for values "${existingValue}" and "${value}". Add an explicit naming rule instead of stabilizing an arbitrary public const name.` + ); + } + usedConstNames.set(constName, value); + lines.push(`\t${constName} ${enumName} = "${value}"`); } lines.push(`)`); diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 1fed3d176..b21a889e4 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -82,10 +82,11 @@ function uniqueRustPascalIdentifier( fallback: string, reserved: Set = new Set(), ): string { - const base = toRustPascalIdentifier(value, fallback); - let identifier = base; - for (let i = 2; used.has(identifier) || reserved.has(identifier); i++) { - identifier = `${base}${i}`; + const identifier = toRustPascalIdentifier(value, fallback); + if (used.has(identifier) || reserved.has(identifier)) { + throw new Error( + `Generated Rust enum variant identifier "${identifier}" is not unique for value "${value}". Add an explicit naming rule instead of stabilizing an arbitrary public variant name.`, + ); } used.add(identifier); return identifier; From e01f3e90f658d1a122d2e75cf11bd7a7cf848803 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 13 May 2026 13:44:34 -0400 Subject: [PATCH 3/6] Complete denied-permission replay snapshots Add the optional post-denial model turns so denied edit tests can replay deterministically if the CLI asks the model to summarize the denied tool result. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ermission_when_handler_returns_denied.yaml | 26 ++++++++++++++++++ ...ts_permission_and_both_see_the_result.yaml | 27 +++++++++++++++++++ 2 files changed, 53 insertions(+) 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 ef6f60dbe..9e54aa424 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 @@ -22,3 +22,29 @@ conversations: function: name: view arguments: '{"path":"${workdir}/protected.txt"}' + - messages: + - role: system + content: ${system} + - role: user + content: Edit protected.txt and replace 'protected' with 'hacked'. + - role: assistant + content: I'll view the file first, then make the edit. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Editing protected.txt file"}' + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/protected.txt"}' + - role: tool + tool_call_id: toolcall_0 + content: Intent logged + - role: tool + tool_call_id: toolcall_1 + content: Permission denied and could not request permission from user + - role: assistant + content: I don't have permission to view or edit protected.txt, so I can't make that change. diff --git a/test/snapshots/rust_multi_client/one_client_rejects_permission_and_both_see_the_result.yaml b/test/snapshots/rust_multi_client/one_client_rejects_permission_and_both_see_the_result.yaml index ba9db87d0..46b6d0ce1 100644 --- a/test/snapshots/rust_multi_client/one_client_rejects_permission_and_both_see_the_result.yaml +++ b/test/snapshots/rust_multi_client/one_client_rejects_permission_and_both_see_the_result.yaml @@ -23,3 +23,30 @@ conversations: function: name: view arguments: '{"path":"${workdir}/protected.txt"}' + - messages: + - role: system + content: ${system} + - role: user + content: Edit protected.txt and replace 'protected' with 'hacked'. + - role: assistant + content: I'll help you edit protected.txt to replace 'protected' with 'hacked'. Let me first view the file and then make + the change. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Editing protected.txt file"}' + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/protected.txt"}' + - role: tool + tool_call_id: toolcall_0 + content: Intent logged + - role: tool + tool_call_id: toolcall_1 + content: Permission denied and could not request permission from user + - role: assistant + content: I don't have permission to view or edit protected.txt, so I can't make that change. From cb931dcf2ab13db8765853ecbccd605819fc2cb7 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 13 May 2026 13:55:24 -0400 Subject: [PATCH 4/6] Complete shared denied-permission snapshot Add the optional post-denial model turn for the shared multi-client replay snapshot so SDKs that wait for the final assistant message can replay deterministically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ts_permission_and_both_see_the_result.yaml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 ba9db87d0..46b6d0ce1 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 @@ -23,3 +23,30 @@ conversations: function: name: view arguments: '{"path":"${workdir}/protected.txt"}' + - messages: + - role: system + content: ${system} + - role: user + content: Edit protected.txt and replace 'protected' with 'hacked'. + - role: assistant + content: I'll help you edit protected.txt to replace 'protected' with 'hacked'. Let me first view the file and then make + the change. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Editing protected.txt file"}' + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/protected.txt"}' + - role: tool + tool_call_id: toolcall_0 + content: Intent logged + - role: tool + tool_call_id: toolcall_1 + content: Permission denied and could not request permission from user + - role: assistant + content: I don't have permission to view or edit protected.txt, so I can't make that change. From b39e1a67aa583e2dfdad9745e3059db36324a3c4 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 13 May 2026 14:02:42 -0400 Subject: [PATCH 5/6] Share denied-permission multi-client snapshot Point the Rust denied-permission multi-client E2E test at the shared multi_client snapshot and remove the duplicate Rust-only copy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/tests/e2e/multi_client.rs | 2 +- ...ts_permission_and_both_see_the_result.yaml | 52 ------------------- 2 files changed, 1 insertion(+), 53 deletions(-) delete mode 100644 test/snapshots/rust_multi_client/one_client_rejects_permission_and_both_see_the_result.yaml diff --git a/rust/tests/e2e/multi_client.rs b/rust/tests/e2e/multi_client.rs index 5f5260e7c..85a288eb2 100644 --- a/rust/tests/e2e/multi_client.rs +++ b/rust/tests/e2e/multi_client.rs @@ -193,7 +193,7 @@ async fn one_client_approves_permission_and_both_see_the_result() { #[tokio::test] async fn one_client_rejects_permission_and_both_see_the_result() { with_e2e_context( - "rust_multi_client", + "multi_client", "one_client_rejects_permission_and_both_see_the_result", |ctx| { Box::pin(async move { diff --git a/test/snapshots/rust_multi_client/one_client_rejects_permission_and_both_see_the_result.yaml b/test/snapshots/rust_multi_client/one_client_rejects_permission_and_both_see_the_result.yaml deleted file mode 100644 index 46b6d0ce1..000000000 --- a/test/snapshots/rust_multi_client/one_client_rejects_permission_and_both_see_the_result.yaml +++ /dev/null @@ -1,52 +0,0 @@ -models: - - claude-sonnet-4.5 -conversations: - - messages: - - role: system - content: ${system} - - role: user - content: Edit protected.txt and replace 'protected' with 'hacked'. - - role: assistant - content: I'll help you edit protected.txt to replace 'protected' with 'hacked'. Let me first view the file and then make - the change. - - role: assistant - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Editing protected.txt file"}' - - role: assistant - tool_calls: - - id: toolcall_1 - type: function - function: - name: view - arguments: '{"path":"${workdir}/protected.txt"}' - - messages: - - role: system - content: ${system} - - role: user - content: Edit protected.txt and replace 'protected' with 'hacked'. - - role: assistant - content: I'll help you edit protected.txt to replace 'protected' with 'hacked'. Let me first view the file and then make - the change. - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Editing protected.txt file"}' - - id: toolcall_1 - type: function - function: - name: view - arguments: '{"path":"${workdir}/protected.txt"}' - - role: tool - tool_call_id: toolcall_0 - content: Intent logged - - role: tool - tool_call_id: toolcall_1 - content: Permission denied and could not request permission from user - - role: assistant - content: I don't have permission to view or edit protected.txt, so I can't make that change. From 1ef194ed95400f03555e1befa8474bc317a246e9 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 13 May 2026 14:09:55 -0400 Subject: [PATCH 6/6] Share approved-permission multi-client snapshot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/tests/e2e/multi_client.rs | 2 +- ...es_permission_and_both_see_the_result.yaml | 50 ------------------- 2 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 test/snapshots/rust_multi_client/one_client_approves_permission_and_both_see_the_result.yaml diff --git a/rust/tests/e2e/multi_client.rs b/rust/tests/e2e/multi_client.rs index 85a288eb2..7d1b61b30 100644 --- a/rust/tests/e2e/multi_client.rs +++ b/rust/tests/e2e/multi_client.rs @@ -105,7 +105,7 @@ async fn both_clients_see_tool_request_and_completion_events() { #[tokio::test] async fn one_client_approves_permission_and_both_see_the_result() { with_e2e_context( - "rust_multi_client", + "multi_client", "one_client_approves_permission_and_both_see_the_result", |ctx| { Box::pin(async move { diff --git a/test/snapshots/rust_multi_client/one_client_approves_permission_and_both_see_the_result.yaml b/test/snapshots/rust_multi_client/one_client_approves_permission_and_both_see_the_result.yaml deleted file mode 100644 index e67357589..000000000 --- a/test/snapshots/rust_multi_client/one_client_approves_permission_and_both_see_the_result.yaml +++ /dev/null @@ -1,50 +0,0 @@ -models: - - claude-sonnet-4.5 -conversations: - - messages: - - role: system - content: ${system} - - role: user - content: Create a file called hello.txt containing the text 'hello world' - - role: assistant - content: I'll create the hello.txt file for you. - - role: assistant - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Creating hello.txt file"}' - - role: assistant - tool_calls: - - id: toolcall_1 - type: function - function: - name: create - arguments: '{"file_text":"hello world","path":"${workdir}/hello.txt"}' - - messages: - - role: system - content: ${system} - - role: user - content: Create a file called hello.txt containing the text 'hello world' - - role: assistant - content: I'll create the hello.txt file for you. - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Creating hello.txt file"}' - - id: toolcall_1 - type: function - function: - name: create - arguments: '{"file_text":"hello world","path":"${workdir}/hello.txt"}' - - role: tool - tool_call_id: toolcall_0 - content: Intent logged - - role: tool - tool_call_id: toolcall_1 - content: Created file ${workdir}/hello.txt with 11 characters - - role: assistant - content: Done - I created hello.txt containing "hello world".