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/rust/tests/e2e/multi_client.rs b/rust/tests/e2e/multi_client.rs index 5f5260e7c..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 { @@ -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/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index edfdd81b1..a1401e1a5 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -118,18 +118,42 @@ 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 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; } async function formatCSharpFile(filePath: string): Promise { @@ -311,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}`; @@ -374,9 +399,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(STRING_ENUM_RESERVED_MEMBER_NAMES); 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..b1a8fb080 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); } @@ -547,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(`)`); @@ -558,14 +568,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..b21a889e4 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -59,11 +59,39 @@ 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 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; +} + function toSnakeCase(s: string): string { return s .replace(/([A-Z])/g, "_$1") @@ -233,10 +261,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 +651,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 +692,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 +968,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); } 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. 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_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". 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 ba9db87d0..000000000 --- a/test/snapshots/rust_multi_client/one_client_rejects_permission_and_both_see_the_result.yaml +++ /dev/null @@ -1,25 +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"}'