diff --git a/CLI/CMUXCLI+AutoNaming.swift b/CLI/CMUXCLI+AutoNaming.swift index 0fc1bdc71589..bc257fadd0c9 100644 --- a/CLI/CMUXCLI+AutoNaming.swift +++ b/CLI/CMUXCLI+AutoNaming.swift @@ -163,6 +163,82 @@ struct AutoNamingEnvironmentPolicy: Sendable { } } +/// Builds the isolated Codex invocation used for workspace naming. +/// +/// `--ignore-user-config` keeps tools, MCP servers, and rules out of the +/// summarizer, but it also removes the user's model provider. Re-apply only +/// the provider selection, its provider table, and the selected model. +struct CodexAutoNamingArguments: Sendable { + static func build(configToml: String?) -> [String] { + var arguments = [ + "exec", + "-c", "default_tools_enabled=false", + "-c", "tools={}", + "-c", "mcp_servers={}", + "-c", "web_search=\"disabled\"", + "-c", "approval_policy=never", + "-c", "shell_environment_policy.inherit=none", + "--skip-git-repo-check", + "--ephemeral", + "--ignore-user-config", + "--ignore-rules", + "--sandbox", "read-only" + ] + guard let configToml else { return arguments } + let overrides = providerOverrides(from: configToml) + for override in overrides.reversed() { + arguments.insert(contentsOf: ["-c", override], at: 1) + } + return arguments + } + + private static func providerOverrides(from toml: String) -> [String] { + var model: String? + var modelProvider: String? + var providerEntries: [(section: String, key: String, value: String)] = [] + var section = "" + for rawLine in toml.split(whereSeparator: \.isNewline) { + let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines) + guard !line.isEmpty, !line.hasPrefix("#") else { continue } + if line.first == "[", line.last == "]" { + section = String(line.dropFirst().dropLast()) + continue + } + guard let equals = line.firstIndex(of: "=") else { continue } + let key = line[.. String? { + guard value.count >= 2, value.first == "\"", value.last == "\"" else { return nil } + return String(value.dropFirst().dropLast()) + } +} + /// Pure auto-naming logic: throttle decisions, transcript extraction, /// prompt construction, and response sanitization. struct AutoNamingEngine: Sendable { diff --git a/CLI/CMUXCLI+AutoNamingDispatch.swift b/CLI/CMUXCLI+AutoNamingDispatch.swift index 112022a7c216..a4f8ad584c37 100644 --- a/CLI/CMUXCLI+AutoNamingDispatch.swift +++ b/CLI/CMUXCLI+AutoNamingDispatch.swift @@ -148,25 +148,17 @@ extension CMUXCLI { try? FileManager.default.removeItem(at: outputFile) try? FileManager.default.removeItem(at: workingDirectory) } + var arguments = CodexAutoNamingArguments.build( + configToml: codexConfigToml(from: summarizerEnv) + ) + arguments += [ + "--cd", workingDirectory.path, + "--output-last-message", outputFile.path, + "-" + ] guard runAutoNamingSummarizer( executable: executable, - arguments: [ - "exec", - "-c", "default_tools_enabled=false", - "-c", "tools={}", - "-c", "mcp_servers={}", - "-c", "web_search=false", - "-c", "approval_policy=never", - "-c", "shell_environment_policy.inherit=none", - "--skip-git-repo-check", - "--ephemeral", - "--ignore-user-config", - "--ignore-rules", - "--sandbox", "read-only", - "--cd", workingDirectory.path, - "--output-last-message", outputFile.path, - "-" - ], + arguments: arguments, prompt: prompt, environment: summarizerEnv, timeout: timeout @@ -175,4 +167,11 @@ extension CMUXCLI { } return (try? String(contentsOf: outputFile, encoding: .utf8)) ?? "" } + + private func codexConfigToml(from env: [String: String]) -> String? { + let home = env["CODEX_HOME"] ?? + ((env["HOME"].map { $0 + "/.codex" }) ?? "") + guard !home.isEmpty else { return nil } + return try? String(contentsOfFile: home + "/config.toml", encoding: .utf8) + } } diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 2f6302cf0e49..4469f1e58b41 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -1476,6 +1476,7 @@ A9F200000000000000000016 /* CodexAppServerQueuedInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F100000000000000000016 /* CodexAppServerQueuedInput.swift */; }; A9E02000000000000000000E /* CodexAppServerSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E01000000000000000000E /* CodexAppServerSession.swift */; }; A9E040000000000000000002 /* CodexAppServerSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E040000000000000000001 /* CodexAppServerSessionTests.swift */; }; + 1606FB10C21C603790FEE7D3 /* CodexAutoNamingArgumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A62542679E9A9BFDD99A7BC6 /* CodexAutoNamingArgumentsTests.swift */; }; B6F143F6E02543F3899F1E02 /* CodexAutoresumeChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F143F6E02543F3899F1E01 /* CodexAutoresumeChainTests.swift */; }; C8711B000000000000000002 /* CodexCodeModeRolloutIdentityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8711B000000000000000001 /* CodexCodeModeRolloutIdentityTests.swift */; }; 9167FFA2759E9AD60F49616C /* CodexHookCapturedSocketCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1893BDBF86104694F22BC3C /* CodexHookCapturedSocketCommands.swift */; }; @@ -5605,6 +5606,7 @@ A9F100000000000000000016 /* CodexAppServerQueuedInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/CodexAppServerQueuedInput.swift; sourceTree = ""; }; A9E01000000000000000000E /* CodexAppServerSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/CodexAppServerSession.swift; sourceTree = ""; }; A9E040000000000000000001 /* CodexAppServerSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexAppServerSessionTests.swift; sourceTree = ""; }; + A62542679E9A9BFDD99A7BC6 /* CodexAutoNamingArgumentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexAutoNamingArgumentsTests.swift; sourceTree = ""; }; B6F143F6E02543F3899F1E01 /* CodexAutoresumeChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexAutoresumeChainTests.swift; sourceTree = ""; }; C8711B000000000000000001 /* CodexCodeModeRolloutIdentityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexCodeModeRolloutIdentityTests.swift; sourceTree = ""; }; D1893BDBF86104694F22BC3C /* CodexHookCapturedSocketCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodexHookCapturedSocketCommands.swift"; sourceTree = ""; }; @@ -8483,6 +8485,7 @@ C11757000000000000000023 /* CodexTeamsResumedBackfillTests.swift */, C11757000000000000000024 /* CodexTerminalErrorNotificationTests.swift */, C11757000000000000000025 /* KimiHookConfigLocationTests.swift */, + A62542679E9A9BFDD99A7BC6 /* CodexAutoNamingArgumentsTests.swift */, ); path = cmuxCLITests; sourceTree = ""; @@ -13297,7 +13300,9 @@ C11757000000000000000036 /* CLIWorkspaceGroupSafetyTests.swift in Sources */, C11757000000000000000048 /* CLIWorkspaceStableIDMockServer.swift in Sources */, C11757000000000000000037 /* CLIWorkspaceStableIDTests.swift in Sources */, + 0CB4E9797AD54D3BB9CF06F9 /* CMUXCLI+AutoNaming.swift in Sources */, C11757000000000000000038 /* CMUXOpenHTMLFocusTests.swift in Sources */, + 1606FB10C21C603790FEE7D3 /* CodexAutoNamingArgumentsTests.swift in Sources */, C1175700000000000000004A /* CodexHookCapturedSocketCommands.swift in Sources */, C1175700000000000000004B /* CodexTeamsAppServerFixture.swift in Sources */, C11757000000000000000061 /* CodexTeamsAppServerProcess.swift in Sources */, diff --git a/cmuxCLITests/CodexAutoNamingArgumentsTests.swift b/cmuxCLITests/CodexAutoNamingArgumentsTests.swift new file mode 100644 index 000000000000..3c1ff16f3ac7 --- /dev/null +++ b/cmuxCLITests/CodexAutoNamingArgumentsTests.swift @@ -0,0 +1,45 @@ +import Testing + +struct CodexAutoNamingArgumentsTests { + @Test func disablesFeaturesAndForwardsSelectedProviderAndModel() { + let args = CodexAutoNamingArguments.build(configToml: """ + model = "gpt-5-codex" + model_provider = "subrouter" + [model_providers.subrouter] + name = "Subrouter" + base_url = "http://127.0.0.1:31415/v1" + experimental_bearer_token = "secret" + [model_providers.subrouter.http_headers] + X-Subrouter-Agent = "sr" + [profiles.default] + model = "ignored" + """) + let overrides = configOverrides(args) + #expect(overrides.contains("web_search=\"disabled\"")) + #expect(!overrides.contains("web_search=false")) + #expect(overrides.contains("model_provider=\"subrouter\"")) + #expect(overrides.contains("model=\"gpt-5-codex\"")) + #expect(overrides.contains("model_providers.subrouter.base_url=\"http://127.0.0.1:31415/v1\"")) + #expect(overrides.contains("model_providers.subrouter.experimental_bearer_token=\"secret\"")) + #expect(overrides.contains("model_providers.subrouter.http_headers.X-Subrouter-Agent=\"sr\"")) + #expect(!overrides.contains(where: { $0.contains("profiles") })) + #expect(args.contains("--ignore-user-config")) + #expect(args.contains("--ignore-rules")) + } + + @Test func keepsIsolationWhenUserConfigIsMissing() { + let args = CodexAutoNamingArguments.build(configToml: nil) + let overrides = configOverrides(args) + #expect(overrides.contains("default_tools_enabled=false")) + #expect(overrides.contains("tools={}")) + #expect(overrides.contains("mcp_servers={}")) + #expect(overrides.contains("web_search=\"disabled\"")) + #expect(!overrides.contains(where: { $0.hasPrefix("model_provider=") })) + } + + private func configOverrides(_ args: [String]) -> [String] { + zip(args, args.dropFirst()).compactMap { pair in + pair.0 == "-c" ? pair.1 : nil + } + } +}