diff --git a/CodexSharpSDK.Tests/Unit/CodexFeaturesTests.cs b/CodexSharpSDK.Tests/Unit/CodexFeaturesTests.cs new file mode 100644 index 0000000..821a602 --- /dev/null +++ b/CodexSharpSDK.Tests/Unit/CodexFeaturesTests.cs @@ -0,0 +1,119 @@ +using System.Reflection; +using System.Text.Json; +using ManagedCode.CodexSharpSDK.Models; + +namespace ManagedCode.CodexSharpSDK.Tests.Unit; + +public class CodexFeaturesTests +{ + private const string SolutionFileName = "ManagedCode.CodexSharpSDK.slnx"; + private const string BundledConfigSchemaFileName = "config.schema.json"; + + [Test] + public async Task CodexFeatures_NewUpstreamFlags_ArePresent() + { + // These two flags were added in upstream commit 3b5fe5c and are the primary + // motivation for this sync. Verify the constants are present and map to the correct + // upstream key strings by resolving them via reflection (avoiding a constant-vs-constant + // comparison that the analyzer rightly flags as a no-op assertion). + var sdkValues = GetSdkFeatureValues(); + await Assert.That(sdkValues).Contains("guardian_approval"); + await Assert.That(sdkValues).Contains("tool_call_mcp_elicitation"); + } + + [Test] + public async Task CodexFeatures_AllConstantsAreValidUpstreamFeatureKeys() + { + var schemaFeatureKeys = await ReadBundledSchemaFeatureKeysAsync(); + var sdkFeatureValues = GetSdkFeatureValues(); + var invalidKeys = sdkFeatureValues + .Except(schemaFeatureKeys, StringComparer.Ordinal) + .ToArray(); + + await Assert.That(invalidKeys).IsEmpty(); + } + + [Test] + public async Task CodexFeatures_CoversAllCanonicalUpstreamFeatureKeys() + { + // The canonical (non-alias) keys from features.rs must all have an SDK constant so + // that callers can reference them without magic strings. + var schemaFeatureKeys = await ReadBundledSchemaFeatureKeysAsync(); + var sdkFeatureValues = GetSdkFeatureValues(); + + // Legacy alias keys that exist in config.schema.json but are NOT canonical feature + // keys in features.rs; they are intentionally excluded from CodexFeatures. + var knownAliases = new HashSet(StringComparer.Ordinal) + { + "collab", + "connectors", + "enable_experimental_windows_sandbox", + "experimental_use_freeform_apply_patch", + "experimental_use_unified_exec_tool", + "include_apply_patch_tool", + "memory_tool", + "web_search", + }; + + var canonicalKeys = schemaFeatureKeys + .Except(knownAliases, StringComparer.Ordinal) + .ToArray(); + + var missingKeys = canonicalKeys + .Except(sdkFeatureValues, StringComparer.Ordinal) + .ToArray(); + + await Assert.That(missingKeys).IsEmpty(); + } + + private static string[] GetSdkFeatureValues() + { + return typeof(CodexFeatures) + .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(field => field is { IsLiteral: true, IsInitOnly: false } && field.FieldType == typeof(string)) + .Select(field => (string)field.GetRawConstantValue()!) + .ToArray(); + } + + private static async Task ReadBundledSchemaFeatureKeysAsync() + { + var schemaPath = ResolveBundledConfigSchemaFilePath(); + using var stream = File.OpenRead(schemaPath); + using var document = await JsonDocument.ParseAsync(stream); + + return document.RootElement + .GetProperty("properties") + .GetProperty("features") + .GetProperty("properties") + .EnumerateObject() + .Select(p => p.Name) + .ToArray(); + } + + private static string ResolveBundledConfigSchemaFilePath() + { + return Path.Combine( + ResolveRepositoryRootPath(), + "submodules", + "openai-codex", + "codex-rs", + "core", + BundledConfigSchemaFileName); + } + + private static string ResolveRepositoryRootPath() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current is not null) + { + if (File.Exists(Path.Combine(current.FullName, SolutionFileName))) + { + return current.FullName; + } + + current = current.Parent; + } + + throw new InvalidOperationException("Could not locate repository root from test execution directory."); + } +} diff --git a/CodexSharpSDK/Models/CodexFeatures.cs b/CodexSharpSDK/Models/CodexFeatures.cs new file mode 100644 index 0000000..b9bdad2 --- /dev/null +++ b/CodexSharpSDK/Models/CodexFeatures.cs @@ -0,0 +1,71 @@ +namespace ManagedCode.CodexSharpSDK.Models; + +/// +/// Canonical feature flag keys for use with +/// and (and their equivalents in +/// ). Values match the keys accepted by the Codex CLI +/// --enable / --disable flags and the [features] section of +/// config.toml. Sourced from the bundled upstream +/// codex-rs/core/src/features.rs. +/// +public static class CodexFeatures +{ + public const string ApplyPatchFreeform = "apply_patch_freeform"; + public const string Apps = "apps"; + public const string AppsMcpGateway = "apps_mcp_gateway"; + public const string Artifact = "artifact"; + public const string ChildAgentsMd = "child_agents_md"; + public const string CodexGitCommit = "codex_git_commit"; + public const string CollaborationModes = "collaboration_modes"; + public const string DefaultModeRequestUserInput = "default_mode_request_user_input"; + public const string ElevatedWindowsSandbox = "elevated_windows_sandbox"; + public const string EnableRequestCompression = "enable_request_compression"; + public const string ExperimentalWindowsSandbox = "experimental_windows_sandbox"; + public const string FastMode = "fast_mode"; + + /// + /// Guardian subagent approval: lets a guardian subagent review on-request approval + /// prompts instead of surfacing them to the user, including sandbox escapes and blocked + /// network access. Experimental feature added in upstream commit 3b5fe5c. + /// + public const string GuardianApproval = "guardian_approval"; + + public const string ImageDetailOriginal = "image_detail_original"; + public const string ImageGeneration = "image_generation"; + public const string JsRepl = "js_repl"; + public const string JsReplToolsOnly = "js_repl_tools_only"; + public const string Memories = "memories"; + public const string MultiAgent = "multi_agent"; + public const string Personality = "personality"; + public const string Plugins = "plugins"; + public const string PowershellUtf8 = "powershell_utf8"; + public const string PreventIdleSleep = "prevent_idle_sleep"; + public const string RealtimeConversation = "realtime_conversation"; + public const string RemoteModels = "remote_models"; + public const string RequestPermissions = "request_permissions"; + public const string RequestRule = "request_rule"; + public const string ResponsesWebsockets = "responses_websockets"; + public const string ResponsesWebsocketsV2 = "responses_websockets_v2"; + public const string RuntimeMetrics = "runtime_metrics"; + public const string SearchTool = "search_tool"; + public const string ShellSnapshot = "shell_snapshot"; + public const string ShellTool = "shell_tool"; + public const string ShellZshFork = "shell_zsh_fork"; + public const string SkillEnvVarDependencyPrompt = "skill_env_var_dependency_prompt"; + public const string SkillMcpDependencyInstall = "skill_mcp_dependency_install"; + public const string Sqlite = "sqlite"; + public const string Steer = "steer"; + + /// + /// Routes MCP tool approval prompts through the MCP elicitation request path. + /// Under-development feature added in upstream commit 3b5fe5c. + /// + public const string ToolCallMcpElicitation = "tool_call_mcp_elicitation"; + + public const string Undo = "undo"; + public const string UnifiedExec = "unified_exec"; + public const string UseLinuxSandboxBwrap = "use_linux_sandbox_bwrap"; + public const string VoiceTranscription = "voice_transcription"; + public const string WebSearchCached = "web_search_cached"; + public const string WebSearchRequest = "web_search_request"; +} diff --git a/global.json b/global.json index c335052..062772b 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.103", + "version": "10.0.102", "rollForward": "latestFeature" }, "test": { diff --git a/submodules/openai-codex b/submodules/openai-codex index 6638558..3b5fe5c 160000 --- a/submodules/openai-codex +++ b/submodules/openai-codex @@ -1 +1 @@ -Subproject commit 6638558b8807328e852b54580b010be7034699b7 +Subproject commit 3b5fe5ca35d914645a818d454a3931f6748b7e77