From 9afdb4356753e46a3304012465a03c82cc5ec133 Mon Sep 17 00:00:00 2001 From: Allen Houchins <32207388+allenhouchins@users.noreply.github.com> Date: Fri, 15 May 2026 13:20:10 -0500 Subject: [PATCH] Add Codex CLI as a Windows FMA (#42397) ## Summary by CodeRabbit * **New Features** * Added comprehensive support for managing Codex CLI (OpenAI's coding agent) on Windows systems, including automated installation, uninstallation, and verification that installed binaries match expected versions * Integrated Codex CLI icon component into the software interface for improved visual identification and enhanced user experience when managing this application [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/42397) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cursor --- cmd/maintained-apps/validate/windows.go | 68 ++++++++++++++++++ .../ingesters/winget/ingester.go | 5 ++ .../inputs/winget/codex-cli.json | 13 ++++ .../winget/scripts/codex-cli_install.ps1 | 59 +++++++++++++++ .../winget/scripts/codex-cli_uninstall.ps1 | 13 ++++ ee/maintained-apps/outputs/apps.json | 7 ++ .../outputs/codex-cli/windows.json | 22 ++++++ .../components/icons/CodexCli.tsx | 14 ++++ .../SoftwarePage/components/icons/index.ts | 2 + .../images/app-icon-codex-cli-60x60@2x.png | Bin 0 -> 6034 bytes 10 files changed, 203 insertions(+) create mode 100644 ee/maintained-apps/inputs/winget/codex-cli.json create mode 100644 ee/maintained-apps/inputs/winget/scripts/codex-cli_install.ps1 create mode 100644 ee/maintained-apps/inputs/winget/scripts/codex-cli_uninstall.ps1 create mode 100644 ee/maintained-apps/outputs/codex-cli/windows.json create mode 100644 frontend/pages/SoftwarePage/components/icons/CodexCli.tsx create mode 100644 website/assets/images/app-icon-codex-cli-60x60@2x.png diff --git a/cmd/maintained-apps/validate/windows.go b/cmd/maintained-apps/validate/windows.go index 50ca675cf8..dafb4feb3e 100644 --- a/cmd/maintained-apps/validate/windows.go +++ b/cmd/maintained-apps/validate/windows.go @@ -174,6 +174,74 @@ func appExists(ctx context.Context, logger *slog.Logger, appName, uniqueIdentifi } } + // OpenAI Codex CLI is a portable zip: it does not register in programs. Detect the binary via osquery file + PE version. + if uniqueIdentifier == "Codex CLI" { + ok, err := codexCLIExistsFromFile(execTimeout, logger, appVersion, appPath) + if err != nil { + return false, err + } + if ok { + return true, nil + } + } + + return false, nil +} + +func codexCLIExistsFromFile(ctx context.Context, logger *slog.Logger, appVersion, appPath string) (bool, error) { + candidates := make([]string, 0, 3) + if appPath != "" { + candidates = append(candidates, filepath.Join(appPath, "codex.exe")) + } + if pf := os.Getenv("ProgramFiles"); pf != "" { + candidates = append(candidates, filepath.Join(pf, "Codex CLI", "codex.exe")) + } + if la := os.Getenv("LOCALAPPDATA"); la != "" { + candidates = append(candidates, filepath.Join(la, "Programs", "Codex CLI", "codex.exe")) + } + + seen := make(map[string]struct{}) + for _, exePath := range candidates { + if _, dup := seen[exePath]; dup { + continue + } + seen[exePath] = struct{}{} + + if err := validateSqlInput(exePath); err != nil { + continue + } + + escaped := strings.ReplaceAll(exePath, "'", "''") + query := `SELECT file_version FROM file WHERE path = '` + escaped + `'` + cmd := exec.CommandContext(ctx, "osqueryi", "--json", query) + output, err := cmd.CombinedOutput() + if err != nil { + logger.ErrorContext(ctx, fmt.Sprintf("osquery output: %s", string(output))) + return false, fmt.Errorf("executing osquery file lookup: %w", err) + } + + type fileResult struct { + FileVersion string `json:"file_version"` + } + var results []fileResult + if err := json.Unmarshal(output, &results); err != nil { + logger.ErrorContext(ctx, fmt.Sprintf("osquery output: %s", string(output))) + return false, fmt.Errorf("parsing osquery JSON output: %w", err) + } + if len(results) == 0 || results[0].FileVersion == "" { + continue + } + + fileVer := results[0].FileVersion + logger.InfoContext(ctx, fmt.Sprintf("Found Codex CLI binary at %s, file version: %s", exePath, fileVer)) + + if fileVer == appVersion || + strings.HasPrefix(fileVer, appVersion+".") || + strings.HasPrefix(appVersion, fileVer+".") { + return true, nil + } + } + return false, nil } diff --git a/ee/maintained-apps/ingesters/winget/ingester.go b/ee/maintained-apps/ingesters/winget/ingester.go index 99cbde9ca0..204c55e1ab 100644 --- a/ee/maintained-apps/ingesters/winget/ingester.go +++ b/ee/maintained-apps/ingesters/winget/ingester.go @@ -381,6 +381,9 @@ func (i *wingetIngester) ingestOne(ctx context.Context, input inputApp) (*mainta } out.Queries = setUpExistsQuery(input.FuzzyMatchName, name, publisher) + if input.ExistsQuery != "" { + out.Queries.Exists = input.ExistsQuery + } out.InstallScript = installScript processedUninstallScript, err := preProcessUninstallScript(uninstallScript, productCode) if err != nil { @@ -515,6 +518,8 @@ type inputApp struct { ProgramPublisher string `json:"program_publisher"` UninstallType string `json:"uninstall_type"` FuzzyMatchName fuzzyMatch `json:"fuzzy_match_name"` + // ExistsQuery overrides the default programs-table exists query (e.g. portable zip installs). + ExistsQuery string `json:"exists_query,omitempty"` // Whether to use "no_check" instead of the app's hash (e.g. for non-pinned download URLs) IgnoreHash bool `json:"ignore_hash"` DefaultCategories []string `json:"default_categories"` diff --git a/ee/maintained-apps/inputs/winget/codex-cli.json b/ee/maintained-apps/inputs/winget/codex-cli.json new file mode 100644 index 0000000000..bd800cb6b3 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/codex-cli.json @@ -0,0 +1,13 @@ +{ + "name": "Codex CLI", + "slug": "codex-cli/windows", + "package_identifier": "OpenAI.Codex", + "unique_identifier": "Codex CLI", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/codex-cli_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/codex-cli_uninstall.ps1", + "exists_query": "SELECT 1 FROM file WHERE path = 'C:\\Program Files\\Codex CLI\\codex.exe' OR path LIKE '%\\AppData\\Local\\Programs\\Codex CLI\\codex.exe';", + "installer_arch": "x64", + "installer_type": "zip", + "installer_scope": "user", + "default_categories": ["Developer tools"] +} diff --git a/ee/maintained-apps/inputs/winget/scripts/codex-cli_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/codex-cli_install.ps1 new file mode 100644 index 0000000000..000aca9fca --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/codex-cli_install.ps1 @@ -0,0 +1,59 @@ +# Codex ships as a .zip of portable binaries (winget NestedInstallerType: portable). +# INSTALLER_PATH points at the downloaded .zip. +# Prefer Program Files (matches Fleet validation + managed installs); fall back to %LOCALAPPDATA% without admin. + +$ErrorActionPreference = "Stop" +$zipPath = "${env:INSTALLER_PATH}" +$machineRoot = Join-Path $env:ProgramFiles "Codex CLI" +$userRoot = Join-Path $env:LOCALAPPDATA "Programs\Codex CLI" +$extractDir = Join-Path $env:TEMP ("codex-winget-extract-" + [Guid]::NewGuid().ToString()) + +if (-not (Test-Path -LiteralPath $machineRoot)) { + try { + New-Item -ItemType Directory -Path $machineRoot -Force -ErrorAction Stop | Out-Null + $installRoot = $machineRoot + } catch { + New-Item -ItemType Directory -Path $userRoot -Force | Out-Null + $installRoot = $userRoot + } +} else { + $installRoot = $machineRoot +} + +try { + if (-not (Test-Path -LiteralPath $zipPath)) { + Write-Host "Installer not found: $zipPath" + Exit 1 + } + + New-Item -ItemType Directory -Path $installRoot -Force | Out-Null + New-Item -ItemType Directory -Path $extractDir -Force | Out-Null + Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDir -Force + + # Matches winget manifest NestedInstallerFiles for x64 + $mainExe = Join-Path $extractDir "codex-x86_64-pc-windows-msvc.exe" + if (-not (Test-Path -LiteralPath $mainExe)) { + Write-Host "Expected binary codex-x86_64-pc-windows-msvc.exe not found in archive" + Exit 1 + } + + $destExe = Join-Path $installRoot "codex.exe" + Copy-Item -LiteralPath $mainExe -Destination $destExe -Force + + foreach ($extra in @( + "codex-command-runner.exe", + "codex-windows-sandbox-setup.exe" + )) { + $src = Join-Path $extractDir $extra + if (Test-Path -LiteralPath $src) { + Copy-Item -LiteralPath $src -Destination (Join-Path $installRoot $extra) -Force + } + } + + Exit 0 +} catch { + Write-Host "Error: $_" + Exit 1 +} finally { + Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/ee/maintained-apps/inputs/winget/scripts/codex-cli_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/codex-cli_uninstall.ps1 new file mode 100644 index 0000000000..0d3734c0e1 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/codex-cli_uninstall.ps1 @@ -0,0 +1,13 @@ +# Removes layouts created by codex-cli_install.ps1 (machine and per-user fallbacks). + +$ErrorActionPreference = "Continue" +foreach ($installRoot in @( + (Join-Path $env:ProgramFiles "Codex CLI"), + (Join-Path $env:LOCALAPPDATA "Programs\Codex CLI") +)) { + if (Test-Path -LiteralPath $installRoot) { + Remove-Item -LiteralPath $installRoot -Recurse -Force + } +} + +Exit 0 diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index a59d64cb7b..e7b0fdb3c0 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -463,6 +463,13 @@ "unique_identifier": "Cloudflare WARP", "description": "Cloudflare WARP enhances internet safety and performance by encrypting your data and optimizing connections for privacy." }, + { + "name": "Codex CLI", + "slug": "codex-cli/windows", + "platform": "windows", + "unique_identifier": "Codex CLI", + "description": "Codex CLI is OpenAI’s coding agent that you can run locally from your terminal." + }, { "name": "Connect Fonts", "slug": "connect-fonts/darwin", diff --git a/ee/maintained-apps/outputs/codex-cli/windows.json b/ee/maintained-apps/outputs/codex-cli/windows.json new file mode 100644 index 0000000000..3d3518f260 --- /dev/null +++ b/ee/maintained-apps/outputs/codex-cli/windows.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "0.116.0", + "queries": { + "exists": "SELECT 1 FROM file WHERE path = 'C:\\Program Files\\Codex CLI\\codex.exe' OR path LIKE '%\\AppData\\Local\\Programs\\Codex CLI\\codex.exe';", + "patch": "" + }, + "installer_url": "https://github.com/openai/codex/releases/download/rust-v0.116.0/codex-x86_64-pc-windows-msvc.exe.zip", + "install_script_ref": "8bb4b68b", + "uninstall_script_ref": "7e1fe544", + "sha256": "f3a64ca3d389224404c1eb654015a868247e4f59bd300e3cdc05f8933f667f8a", + "default_categories": [ + "Developer tools" + ] + } + ], + "refs": { + "7e1fe544": "# Removes layouts created by codex-cli_install.ps1 (machine and per-user fallbacks).\n\n$ErrorActionPreference = \"Continue\"\nforeach ($installRoot in @(\n (Join-Path $env:ProgramFiles \"Codex CLI\"),\n (Join-Path $env:LOCALAPPDATA \"Programs\\Codex CLI\")\n)) {\n if (Test-Path -LiteralPath $installRoot) {\n Remove-Item -LiteralPath $installRoot -Recurse -Force\n }\n}\n\nExit 0\n", + "8bb4b68b": "# Codex ships as a .zip of portable binaries (winget NestedInstallerType: portable).\n# INSTALLER_PATH points at the downloaded .zip.\n# Prefer Program Files (matches Fleet validation + managed installs); fall back to %LOCALAPPDATA% without admin.\n\n$ErrorActionPreference = \"Stop\"\n$zipPath = \"${env:INSTALLER_PATH}\"\n$machineRoot = Join-Path $env:ProgramFiles \"Codex CLI\"\n$userRoot = Join-Path $env:LOCALAPPDATA \"Programs\\Codex CLI\"\n$extractDir = Join-Path $env:TEMP (\"codex-winget-extract-\" + [Guid]::NewGuid().ToString())\n\nif (-not (Test-Path -LiteralPath $machineRoot)) {\n try {\n New-Item -ItemType Directory -Path $machineRoot -Force -ErrorAction Stop | Out-Null\n $installRoot = $machineRoot\n } catch {\n New-Item -ItemType Directory -Path $userRoot -Force | Out-Null\n $installRoot = $userRoot\n }\n} else {\n $installRoot = $machineRoot\n}\n\ntry {\n if (-not (Test-Path -LiteralPath $zipPath)) {\n Write-Host \"Installer not found: $zipPath\"\n Exit 1\n }\n\n New-Item -ItemType Directory -Path $installRoot -Force | Out-Null\n New-Item -ItemType Directory -Path $extractDir -Force | Out-Null\n Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDir -Force\n\n # Matches winget manifest NestedInstallerFiles for x64\n $mainExe = Join-Path $extractDir \"codex-x86_64-pc-windows-msvc.exe\"\n if (-not (Test-Path -LiteralPath $mainExe)) {\n Write-Host \"Expected binary codex-x86_64-pc-windows-msvc.exe not found in archive\"\n Exit 1\n }\n\n $destExe = Join-Path $installRoot \"codex.exe\"\n Copy-Item -LiteralPath $mainExe -Destination $destExe -Force\n\n foreach ($extra in @(\n \"codex-command-runner.exe\",\n \"codex-windows-sandbox-setup.exe\"\n )) {\n $src = Join-Path $extractDir $extra\n if (Test-Path -LiteralPath $src) {\n Copy-Item -LiteralPath $src -Destination (Join-Path $installRoot $extra) -Force\n }\n }\n\n Exit 0\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n} finally {\n Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue\n}\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/CodexCli.tsx b/frontend/pages/SoftwarePage/components/icons/CodexCli.tsx new file mode 100644 index 0000000000..b869ce4569 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/CodexCli.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const CodexCli = (props: SVGProps) => ( + + + +); +export default CodexCli; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index 5f723a0e1d..7af7af906e 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -9,6 +9,7 @@ import Backblaze from "./Backblaze"; import BetterDisplay from "./BetterDisplay"; import Cavalry from "./Cavalry"; import Charles from "./Charles"; +import CodexCli from "./CodexCli"; import ConnectFonts from "./ConnectFonts"; import CrashPlan from "./CrashPlan"; import DruvaInSync from "./DruvaInSync"; @@ -320,6 +321,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { "clockify desktop": ClockifyDesktop, cloudflare: Cloudflare, code: VisualStudioCode, + "codex cli": CodexCli, "company portal": IntuneCompanyPortal, "connect fonts": ConnectFonts, crashplan: CrashPlan, diff --git a/website/assets/images/app-icon-codex-cli-60x60@2x.png b/website/assets/images/app-icon-codex-cli-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..03ce31400e382cedff8201bd9221f76b1be5cea7 GIT binary patch literal 6034 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