mirror of
https://github.com/openai/codex.git
synced 2026-09-04 15:08:45 +00:00
Split the code-mode protocol and client from the V8-backed runtime so core and
app-server no longer link codex-code-mode in production. ThreadManager now
provisions durable code-mode sessions through a shared external host process,
while tests can still inject the in-process provider.
The IPC protocol uses a persistent stdin/stdout transport. Each frame is a
4-byte big-endian length followed by JSON, with a 16 MiB frame limit. Client
requests carry u64 request IDs so create, execute, wait, terminate, and shutdown
operations can be multiplexed over one process. Session IDs isolate durable
stored values. Execute returns an ExecutionStarted response immediately and an
asynchronous InitialResponse when the initial yield or completion is available.
Nested tool calls and notifications travel from the host back to the client as
delegate requests with their own IDs. Delegate responses, cancellation, and
cell-closed lifecycle messages use the same framed channel. Wire operations
encode errors as Result values. A dead connection fails pending operations,
cancels outstanding delegates, and lets the provider spawn a new host for later
sessions.
Build codex-code-mode-host with V8 pointer-compression sandbox support and add
it to canonical primary and app-server packages, legacy Linux and Windows
bundles, signing verification, installers, Python runtime packages, and release
CI for macOS, Linux, and Windows. The host is discovered next to the current
executable, through CODEX_CODE_MODE_HOST_PATH, or on PATH. OS-level seccomp or
seatbelt restrictions remain a follow-up to this cross-platform process split.
Benchmarks were run from release builds on Linux x86_64 with the V8 sandbox
profile and a text('ok') workload. Cold measurements used 30 samples, warm
session provisioning used 200, and warm command execution used 500. Values are
mean/p50/p95 in milliseconds:
- session startup: in-process 0.002/0.002/0.005, IPC 2.623/2.599/2.894
- fresh-session command: in-process 1.831/1.758/1.915, IPC 7.428/7.252/8.306
- warm session provisioning: in-process 0.002/0.002/0.003,
IPC 0.471/0.463/0.581
- warm command: in-process 1.759/1.757/1.940, IPC 2.005/2.001/2.166
The steady-state median command overhead is approximately 0.244 ms. The median
fresh host plus first command cost is 7.252 ms.
Validation:
- 62/62 core code-mode integration tests passed against the external host
- focused protocol, client, runtime, host, tools, and trace tests passed
- Cargo and Bazel real-process host IPC tests passed
- 11/11 package builder tests passed
- Bazel lock verification, scoped Clippy fixes, and repository formatting passed
926 lines
29 KiB
PowerShell
926 lines
29 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$Release = $env:CODEX_RELEASE
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
$ProgressPreference = "SilentlyContinue"
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Release)) {
|
|
$Release = "latest"
|
|
}
|
|
|
|
$NonInteractive = $env:CODEX_NON_INTERACTIVE -match "^(?i:1|true|yes)$"
|
|
|
|
function Write-Step {
|
|
param(
|
|
[string]$Message
|
|
)
|
|
|
|
Write-Host "==> $Message"
|
|
}
|
|
|
|
function Write-WarningStep {
|
|
param(
|
|
[string]$Message
|
|
)
|
|
|
|
Write-Warning $Message
|
|
}
|
|
|
|
function Prompt-YesNo {
|
|
param(
|
|
[string]$Prompt
|
|
)
|
|
|
|
if ($NonInteractive) {
|
|
return $false
|
|
}
|
|
|
|
if ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected) {
|
|
return $false
|
|
}
|
|
|
|
$choice = Read-Host "$Prompt [y/N]"
|
|
return $choice -match "^(?i:y(?:es)?)$"
|
|
}
|
|
|
|
function Normalize-Version {
|
|
param(
|
|
[string]$RawVersion
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($RawVersion) -or $RawVersion -eq "latest") {
|
|
return "latest"
|
|
}
|
|
|
|
if ($RawVersion.StartsWith("rust-v")) {
|
|
return $RawVersion.Substring(6)
|
|
}
|
|
|
|
if ($RawVersion.StartsWith("v")) {
|
|
return $RawVersion.Substring(1)
|
|
}
|
|
|
|
return $RawVersion
|
|
}
|
|
|
|
function Assert-ValidReleaseVersion {
|
|
param(
|
|
[string]$Version
|
|
)
|
|
|
|
if ($Version -cne "latest" -and $Version -cnotmatch "^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)(?:\.[0-9]+)?)?$") {
|
|
throw "Invalid Codex release version: $Version. Expected latest or x.y.z[-alpha[.N]|-beta[.N]]."
|
|
}
|
|
}
|
|
|
|
function Find-ReleaseAssetMetadata {
|
|
param(
|
|
[string]$AssetName,
|
|
[string]$ResolvedVersion
|
|
)
|
|
|
|
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/openai/codex/releases/tags/rust-v$ResolvedVersion"
|
|
$asset = $release.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1
|
|
if ($null -eq $asset) {
|
|
return $null
|
|
}
|
|
|
|
$digestMatch = [regex]::Match([string]$asset.digest, "^sha256:([0-9a-fA-F]{64})$")
|
|
if (-not $digestMatch.Success) {
|
|
throw "Could not find SHA-256 digest for release asset $AssetName."
|
|
}
|
|
|
|
return [PSCustomObject]@{
|
|
Url = $asset.browser_download_url
|
|
Sha256 = $digestMatch.Groups[1].Value.ToLowerInvariant()
|
|
}
|
|
}
|
|
|
|
function Get-ReleaseAssetMetadata {
|
|
param(
|
|
[string]$AssetName,
|
|
[string]$ResolvedVersion
|
|
)
|
|
|
|
$metadata = Find-ReleaseAssetMetadata -AssetName $AssetName -ResolvedVersion $ResolvedVersion
|
|
if ($null -eq $metadata) {
|
|
throw "Could not find release asset $AssetName for Codex $ResolvedVersion."
|
|
}
|
|
|
|
return $metadata
|
|
}
|
|
|
|
function Test-ArchiveDigest {
|
|
param(
|
|
[string]$ArchivePath,
|
|
[string]$ExpectedDigest
|
|
)
|
|
|
|
$actualDigest = (Get-FileHash -LiteralPath $ArchivePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
if ($actualDigest -ne $ExpectedDigest) {
|
|
throw "Downloaded Codex archive checksum did not match expected digest. Expected $ExpectedDigest but got $actualDigest."
|
|
}
|
|
}
|
|
|
|
function Get-PackageArchiveDigest {
|
|
param(
|
|
[string]$ManifestPath,
|
|
[string]$AssetName
|
|
)
|
|
|
|
$escapedAssetName = [regex]::Escape($AssetName)
|
|
foreach ($line in Get-Content -LiteralPath $ManifestPath) {
|
|
$match = [regex]::Match($line, "^\s*([0-9a-fA-F]{64})\s+$escapedAssetName\s*$")
|
|
if ($match.Success) {
|
|
return $match.Groups[1].Value.ToLowerInvariant()
|
|
}
|
|
}
|
|
|
|
throw "Could not find SHA-256 digest for $AssetName in codex-package_SHA256SUMS."
|
|
}
|
|
|
|
function Path-Contains {
|
|
param(
|
|
[string]$PathValue,
|
|
[string]$Entry
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($PathValue)) {
|
|
return $false
|
|
}
|
|
|
|
$needle = $Entry.TrimEnd("\")
|
|
foreach ($segment in $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries)) {
|
|
if ($segment.TrimEnd("\") -ieq $needle) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Prepend-PathEntry {
|
|
param(
|
|
[string]$PathValue,
|
|
[string]$Entry
|
|
)
|
|
|
|
$needle = $Entry.TrimEnd("\")
|
|
$segments = @($Entry)
|
|
if (-not [string]::IsNullOrWhiteSpace($PathValue)) {
|
|
$segments += $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) |
|
|
Where-Object { $_.TrimEnd("\") -ine $needle }
|
|
}
|
|
|
|
return ($segments -join ";")
|
|
}
|
|
|
|
function Invoke-WithInstallLock {
|
|
param(
|
|
[string]$LockPath,
|
|
[scriptblock]$Script
|
|
)
|
|
|
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LockPath) | Out-Null
|
|
$lock = $null
|
|
while ($null -eq $lock) {
|
|
try {
|
|
$lock = [System.IO.File]::Open(
|
|
$LockPath,
|
|
[System.IO.FileMode]::OpenOrCreate,
|
|
[System.IO.FileAccess]::ReadWrite,
|
|
[System.IO.FileShare]::None
|
|
)
|
|
} catch [System.IO.IOException] {
|
|
Start-Sleep -Milliseconds 250
|
|
}
|
|
}
|
|
try {
|
|
& $Script
|
|
} finally {
|
|
$lock.Dispose()
|
|
}
|
|
}
|
|
|
|
function Remove-StaleInstallArtifacts {
|
|
param(
|
|
[string]$ReleasesDir
|
|
)
|
|
|
|
if (Test-Path -LiteralPath $ReleasesDir -PathType Container) {
|
|
Get-ChildItem -LiteralPath $ReleasesDir -Force -Directory -Filter ".staging.*" -ErrorAction SilentlyContinue |
|
|
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
function Resolve-Version {
|
|
$normalizedVersion = Normalize-Version -RawVersion $Release
|
|
Assert-ValidReleaseVersion -Version $normalizedVersion
|
|
if ($normalizedVersion -ne "latest") {
|
|
return $normalizedVersion
|
|
}
|
|
|
|
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/openai/codex/releases/latest"
|
|
if (-not $release.tag_name) {
|
|
Write-Error "Failed to resolve the latest Codex release version."
|
|
exit 1
|
|
}
|
|
|
|
$resolvedVersion = Normalize-Version -RawVersion $release.tag_name
|
|
Assert-ValidReleaseVersion -Version $resolvedVersion
|
|
return $resolvedVersion
|
|
}
|
|
|
|
function Get-VersionFromBinary {
|
|
param(
|
|
[string]$CodexPath
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $CodexPath -PathType Leaf)) {
|
|
return $null
|
|
}
|
|
|
|
try {
|
|
$versionOutput = & $CodexPath --version 2>$null
|
|
} catch {
|
|
return $null
|
|
}
|
|
|
|
if ($versionOutput -match '([0-9][0-9A-Za-z.+-]*)$') {
|
|
return $matches[1]
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Get-CurrentInstalledVersion {
|
|
param(
|
|
[string]$StandaloneCurrentDir
|
|
)
|
|
|
|
$standaloneVersion = Get-VersionFromBinary -CodexPath (Join-Path $StandaloneCurrentDir "bin\codex.exe")
|
|
if (-not [string]::IsNullOrWhiteSpace($standaloneVersion)) {
|
|
return $standaloneVersion
|
|
}
|
|
|
|
$standaloneVersion = Get-VersionFromBinary -CodexPath (Join-Path $StandaloneCurrentDir "codex.exe")
|
|
if (-not [string]::IsNullOrWhiteSpace($standaloneVersion)) {
|
|
return $standaloneVersion
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Test-OldStandaloneBinLayout {
|
|
param(
|
|
[string]$VisibleBinDir,
|
|
[string]$DefaultVisibleBinDir
|
|
)
|
|
|
|
if (-not $VisibleBinDir.Equals($DefaultVisibleBinDir, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
return $false
|
|
}
|
|
if (-not (Test-Path -LiteralPath $VisibleBinDir -PathType Container)) {
|
|
return $false
|
|
}
|
|
|
|
$item = Get-Item -LiteralPath $VisibleBinDir -Force
|
|
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
|
return $false
|
|
}
|
|
|
|
$requiredFiles = @("codex.exe", "rg.exe")
|
|
foreach ($fileName in $requiredFiles) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $VisibleBinDir $fileName) -PathType Leaf)) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
$knownFiles = @(
|
|
"codex.exe",
|
|
"rg.exe",
|
|
"codex-command-runner.exe",
|
|
"codex-windows-sandbox.exe",
|
|
"codex-windows-sandbox-setup.exe"
|
|
)
|
|
foreach ($child in Get-ChildItem -LiteralPath $VisibleBinDir -Force) {
|
|
if ($child.PSIsContainer) {
|
|
return $false
|
|
}
|
|
if ($knownFiles -notcontains $child.Name) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Move-OldStandaloneBinIfApproved {
|
|
param(
|
|
[string]$VisibleBinDir,
|
|
[string]$DefaultVisibleBinDir
|
|
)
|
|
|
|
if (-not (Test-OldStandaloneBinLayout -VisibleBinDir $VisibleBinDir -DefaultVisibleBinDir $DefaultVisibleBinDir)) {
|
|
return $null
|
|
}
|
|
|
|
Write-Step "We found an older Codex install at $VisibleBinDir"
|
|
Write-WarningStep "To continue, Codex needs to update the install at this path."
|
|
if (-not (Prompt-YesNo "Replace it with the current Codex setup now?")) {
|
|
throw "Cannot replace older standalone install without confirmation: $VisibleBinDir"
|
|
}
|
|
|
|
$backupDir = "$VisibleBinDir.backup.$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds()).$PID"
|
|
Write-Step "Moving older standalone install to $backupDir"
|
|
Move-Item -LiteralPath $VisibleBinDir -Destination $backupDir
|
|
return $backupDir
|
|
}
|
|
|
|
function Add-JunctionSupportType {
|
|
if (([System.Management.Automation.PSTypeName]'CodexInstaller.Junction').Type) {
|
|
return
|
|
}
|
|
|
|
Add-Type -TypeDefinition @"
|
|
using System;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using Microsoft.Win32.SafeHandles;
|
|
|
|
namespace CodexInstaller
|
|
{
|
|
public static class Junction
|
|
{
|
|
private const uint GENERIC_WRITE = 0x40000000;
|
|
private const uint FILE_SHARE_READ = 0x00000001;
|
|
private const uint FILE_SHARE_WRITE = 0x00000002;
|
|
private const uint FILE_SHARE_DELETE = 0x00000004;
|
|
private const uint OPEN_EXISTING = 3;
|
|
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
|
private const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
|
|
private const uint FSCTL_SET_REPARSE_POINT = 0x000900A4;
|
|
private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
|
|
private const int HeaderLength = 20;
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
private static extern SafeFileHandle CreateFileW(
|
|
string lpFileName,
|
|
uint dwDesiredAccess,
|
|
uint dwShareMode,
|
|
IntPtr lpSecurityAttributes,
|
|
uint dwCreationDisposition,
|
|
uint dwFlagsAndAttributes,
|
|
IntPtr hTemplateFile);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern bool DeviceIoControl(
|
|
SafeFileHandle hDevice,
|
|
uint dwIoControlCode,
|
|
byte[] lpInBuffer,
|
|
int nInBufferSize,
|
|
IntPtr lpOutBuffer,
|
|
int nOutBufferSize,
|
|
out int lpBytesReturned,
|
|
IntPtr lpOverlapped);
|
|
|
|
public static void SetTarget(string linkPath, string targetPath)
|
|
{
|
|
string substituteName = "\\??\\" + Path.GetFullPath(targetPath);
|
|
byte[] substituteNameBytes = Encoding.Unicode.GetBytes(substituteName);
|
|
if (substituteNameBytes.Length > ushort.MaxValue - HeaderLength) {
|
|
throw new ArgumentException("Junction target path is too long.", "targetPath");
|
|
}
|
|
|
|
byte[] reparseBuffer = new byte[substituteNameBytes.Length + HeaderLength];
|
|
WriteUInt32(reparseBuffer, 0, IO_REPARSE_TAG_MOUNT_POINT);
|
|
WriteUInt16(reparseBuffer, 4, checked((ushort)(substituteNameBytes.Length + 12)));
|
|
WriteUInt16(reparseBuffer, 8, 0);
|
|
WriteUInt16(reparseBuffer, 10, checked((ushort)substituteNameBytes.Length));
|
|
WriteUInt16(reparseBuffer, 12, checked((ushort)(substituteNameBytes.Length + 2)));
|
|
WriteUInt16(reparseBuffer, 14, 0);
|
|
Buffer.BlockCopy(substituteNameBytes, 0, reparseBuffer, 16, substituteNameBytes.Length);
|
|
|
|
using (SafeFileHandle handle = CreateFileW(
|
|
linkPath,
|
|
GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
IntPtr.Zero,
|
|
OPEN_EXISTING,
|
|
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
|
|
IntPtr.Zero))
|
|
{
|
|
if (handle.IsInvalid) {
|
|
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
}
|
|
|
|
int bytesReturned;
|
|
if (!DeviceIoControl(
|
|
handle,
|
|
FSCTL_SET_REPARSE_POINT,
|
|
reparseBuffer,
|
|
reparseBuffer.Length,
|
|
IntPtr.Zero,
|
|
0,
|
|
out bytesReturned,
|
|
IntPtr.Zero))
|
|
{
|
|
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void WriteUInt16(byte[] buffer, int offset, ushort value)
|
|
{
|
|
buffer[offset] = (byte)value;
|
|
buffer[offset + 1] = (byte)(value >> 8);
|
|
}
|
|
|
|
private static void WriteUInt32(byte[] buffer, int offset, uint value)
|
|
{
|
|
buffer[offset] = (byte)value;
|
|
buffer[offset + 1] = (byte)(value >> 8);
|
|
buffer[offset + 2] = (byte)(value >> 16);
|
|
buffer[offset + 3] = (byte)(value >> 24);
|
|
}
|
|
}
|
|
}
|
|
"@
|
|
}
|
|
|
|
function Set-JunctionTarget {
|
|
param(
|
|
[string]$LinkPath,
|
|
[string]$TargetPath
|
|
)
|
|
|
|
Add-JunctionSupportType
|
|
[CodexInstaller.Junction]::SetTarget($LinkPath, $TargetPath)
|
|
}
|
|
|
|
function Test-IsJunction {
|
|
param(
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
return $false
|
|
}
|
|
|
|
$item = Get-Item -LiteralPath $Path -Force
|
|
return ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -and $item.LinkType -eq "Junction"
|
|
}
|
|
|
|
function Ensure-Junction {
|
|
param(
|
|
[string]$LinkPath,
|
|
[string]$TargetPath,
|
|
[string]$InstallerOwnedTargetPrefix
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $LinkPath)) {
|
|
New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null
|
|
return
|
|
}
|
|
|
|
$item = Get-Item -LiteralPath $LinkPath -Force
|
|
if (Test-IsJunction -Path $LinkPath) {
|
|
$existingTarget = [string]$item.Target
|
|
if (-not [string]::IsNullOrWhiteSpace($InstallerOwnedTargetPrefix)) {
|
|
$ownedTargetPrefix = $InstallerOwnedTargetPrefix.TrimEnd("\\")
|
|
if (-not $existingTarget.StartsWith($ownedTargetPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Refusing to retarget junction at $LinkPath because it is not managed by this installer."
|
|
}
|
|
}
|
|
if ($existingTarget.Equals($TargetPath, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
return
|
|
}
|
|
|
|
# Keep the path itself in place and only retarget the junction. That
|
|
# avoids a gap where current or the visible bin path disappears during
|
|
# an update.
|
|
Set-JunctionTarget -LinkPath $LinkPath -TargetPath $TargetPath
|
|
return
|
|
}
|
|
|
|
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
|
throw "Refusing to replace non-junction reparse point at $LinkPath."
|
|
}
|
|
|
|
if ($item.PSIsContainer) {
|
|
if ((Get-ChildItem -LiteralPath $LinkPath -Force | Select-Object -First 1) -ne $null) {
|
|
throw "Refusing to replace non-empty directory at $LinkPath with a junction."
|
|
}
|
|
|
|
Remove-Item -LiteralPath $LinkPath -Force
|
|
New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null
|
|
return
|
|
}
|
|
|
|
throw "Refusing to replace file at $LinkPath with a junction."
|
|
}
|
|
|
|
function Test-PackageContentsAreComplete {
|
|
param(
|
|
[string]$PackageDir
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $PackageDir -PathType Container)) {
|
|
return $false
|
|
}
|
|
|
|
$expectedFiles = @(
|
|
"codex-package.json",
|
|
"bin\codex.exe",
|
|
"bin\codex-code-mode-host.exe",
|
|
"codex-path\rg.exe",
|
|
"codex-resources\codex-command-runner.exe",
|
|
"codex-resources\codex-windows-sandbox-setup.exe"
|
|
)
|
|
foreach ($name in $expectedFiles) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $PackageDir $name) -PathType Leaf)) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Test-LegacyPlatformNpmContentsAreComplete {
|
|
param(
|
|
[string]$PackageDir
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $PackageDir -PathType Container)) {
|
|
return $false
|
|
}
|
|
|
|
$expectedFiles = @(
|
|
"codex.exe",
|
|
"codex-resources\codex-command-runner.exe",
|
|
"codex-resources\codex-windows-sandbox-setup.exe",
|
|
"codex-resources\rg.exe"
|
|
)
|
|
foreach ($name in $expectedFiles) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $PackageDir $name) -PathType Leaf)) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Test-ReleaseIsComplete {
|
|
param(
|
|
[string]$ReleaseDir,
|
|
[string]$ExpectedVersion,
|
|
[string]$ExpectedTarget,
|
|
[string]$Layout
|
|
)
|
|
|
|
switch ($Layout) {
|
|
"Package" {
|
|
if (-not (Test-PackageContentsAreComplete -PackageDir $ReleaseDir)) {
|
|
return $false
|
|
}
|
|
}
|
|
"LegacyPlatformNpm" {
|
|
if (-not (Test-LegacyPlatformNpmContentsAreComplete -PackageDir $ReleaseDir)) {
|
|
return $false
|
|
}
|
|
}
|
|
default {
|
|
throw "Unknown Codex installer layout: $Layout"
|
|
}
|
|
}
|
|
|
|
return (Split-Path -Leaf $ReleaseDir) -eq "$ExpectedVersion-$ExpectedTarget"
|
|
}
|
|
|
|
function Get-ExistingCodexCommand {
|
|
$existing = Get-Command codex -ErrorAction SilentlyContinue
|
|
if ($null -eq $existing) {
|
|
return $null
|
|
}
|
|
|
|
return $existing.Source
|
|
}
|
|
|
|
function Get-ExistingCodexManager {
|
|
param(
|
|
[string]$ExistingPath,
|
|
[string]$VisibleBinDir
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ExistingPath)) {
|
|
return $null
|
|
}
|
|
|
|
if ($ExistingPath.StartsWith($VisibleBinDir, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
return $null
|
|
}
|
|
|
|
if ($ExistingPath -match "\\.bun\\") {
|
|
return "bun"
|
|
}
|
|
|
|
if ($ExistingPath -match "node_modules" -or $ExistingPath -match "\\npm\\") {
|
|
return "npm"
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Get-ConflictingInstall {
|
|
param(
|
|
[string]$VisibleBinDir
|
|
)
|
|
|
|
$existingPath = Get-ExistingCodexCommand
|
|
$manager = Get-ExistingCodexManager -ExistingPath $existingPath -VisibleBinDir $VisibleBinDir
|
|
if ($null -eq $manager) {
|
|
return $null
|
|
}
|
|
|
|
Write-Step "Detected existing $manager-managed Codex at $existingPath"
|
|
Write-WarningStep "Multiple managed Codex installs can be ambiguous because PATH order decides which one runs."
|
|
|
|
return [PSCustomObject]@{
|
|
Manager = $manager
|
|
Path = $existingPath
|
|
}
|
|
}
|
|
|
|
function Maybe-HandleConflictingInstall {
|
|
param(
|
|
[object]$Conflict
|
|
)
|
|
|
|
if ($null -eq $Conflict) {
|
|
return
|
|
}
|
|
|
|
$manager = $Conflict.Manager
|
|
|
|
$uninstallArgs = if ($manager -eq "bun") {
|
|
@("remove", "-g", "@openai/codex")
|
|
} else {
|
|
@("uninstall", "-g", "@openai/codex")
|
|
}
|
|
$uninstallCommand = if ($manager -eq "bun") { "bun" } else { "npm" }
|
|
|
|
if (Prompt-YesNo "Uninstall the existing $manager-managed Codex now?") {
|
|
Write-Step "Running: $uninstallCommand $($uninstallArgs -join ' ')"
|
|
try {
|
|
& $uninstallCommand @uninstallArgs
|
|
} catch {
|
|
Write-WarningStep "Failed to uninstall the existing $manager-managed Codex. Continuing with the standalone install."
|
|
}
|
|
} else {
|
|
Write-WarningStep "Leaving the existing $manager-managed Codex installed. PATH order will determine which codex runs."
|
|
}
|
|
}
|
|
|
|
function Test-VisibleCodexCommand {
|
|
param(
|
|
[string]$VisibleBinDir
|
|
)
|
|
|
|
$codexCommand = Join-Path $VisibleBinDir "codex.exe"
|
|
& $codexCommand --version *> $null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Installed Codex command failed verification: $codexCommand --version"
|
|
}
|
|
}
|
|
|
|
if ($env:OS -ne "Windows_NT") {
|
|
Write-Error "install.ps1 supports Windows only. Use install.sh on macOS or Linux."
|
|
exit 1
|
|
}
|
|
|
|
if (-not [Environment]::Is64BitOperatingSystem) {
|
|
Write-Error "Codex requires a 64-bit version of Windows."
|
|
exit 1
|
|
}
|
|
|
|
$architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
|
$target = $null
|
|
$platformLabel = $null
|
|
$npmTag = $null
|
|
switch ($architecture) {
|
|
"Arm64" {
|
|
$target = "aarch64-pc-windows-msvc"
|
|
$platformLabel = "Windows (ARM64)"
|
|
$npmTag = "win32-arm64"
|
|
}
|
|
"X64" {
|
|
$target = "x86_64-pc-windows-msvc"
|
|
$platformLabel = "Windows (x64)"
|
|
$npmTag = "win32-x64"
|
|
}
|
|
default {
|
|
Write-Error "Unsupported architecture: $architecture"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
$codexHome = if ([string]::IsNullOrWhiteSpace($env:CODEX_HOME)) {
|
|
Join-Path $env:USERPROFILE ".codex"
|
|
} else {
|
|
$env:CODEX_HOME
|
|
}
|
|
$standaloneRoot = Join-Path $codexHome "packages\standalone"
|
|
$releasesDir = Join-Path $standaloneRoot "releases"
|
|
$currentDir = Join-Path $standaloneRoot "current"
|
|
$lockPath = Join-Path $standaloneRoot "install.lock"
|
|
|
|
$defaultVisibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\OpenAI\Codex\bin"
|
|
if ([string]::IsNullOrWhiteSpace($env:CODEX_INSTALL_DIR)) {
|
|
$visibleBinDir = $defaultVisibleBinDir
|
|
} else {
|
|
$visibleBinDir = $env:CODEX_INSTALL_DIR
|
|
}
|
|
|
|
$currentVersion = Get-CurrentInstalledVersion -StandaloneCurrentDir $currentDir
|
|
$resolvedVersion = Resolve-Version
|
|
$releaseName = "$resolvedVersion-$target"
|
|
$releaseDir = Join-Path $releasesDir $releaseName
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($currentVersion) -and $currentVersion -ne $resolvedVersion) {
|
|
Write-Step "Updating Codex CLI from $currentVersion to $resolvedVersion"
|
|
} elseif (-not [string]::IsNullOrWhiteSpace($currentVersion)) {
|
|
Write-Step "Updating Codex CLI"
|
|
} else {
|
|
Write-Step "Installing Codex CLI"
|
|
}
|
|
Write-Step "Detected platform: $platformLabel"
|
|
Write-Step "Resolved version: $resolvedVersion"
|
|
|
|
$conflictingInstall = Get-ConflictingInstall -VisibleBinDir $visibleBinDir
|
|
$oldStandaloneBackup = $null
|
|
|
|
$packageAsset = "codex-package-$target.tar.gz"
|
|
$checksumAsset = "codex-package_SHA256SUMS"
|
|
$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion
|
|
$checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ResolvedVersion $resolvedVersion
|
|
$installLayout = "Package"
|
|
if ($null -eq $packageMetadata -or $null -eq $checksumMetadata) {
|
|
$packageAsset = "codex-npm-$npmTag-$resolvedVersion.tgz"
|
|
$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion
|
|
if ($null -ne $packageMetadata) {
|
|
$installLayout = "LegacyPlatformNpm"
|
|
} else {
|
|
throw "Could not find Codex package or platform npm release assets for Codex $resolvedVersion."
|
|
}
|
|
$checksumMetadata = $null
|
|
}
|
|
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-install-" + [System.Guid]::NewGuid().ToString("N"))
|
|
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
|
|
|
|
try {
|
|
Invoke-WithInstallLock -LockPath $lockPath -Script {
|
|
Remove-StaleInstallArtifacts -ReleasesDir $releasesDir
|
|
|
|
if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target -Layout $installLayout)) {
|
|
if (Test-Path -LiteralPath $releaseDir) {
|
|
Write-WarningStep "Found incomplete existing release at $releaseDir. Reinstalling."
|
|
}
|
|
|
|
$archivePath = Join-Path $tempDir $packageAsset
|
|
$checksumPath = Join-Path $tempDir $checksumAsset
|
|
$stagingDir = Join-Path $releasesDir ".staging.$releaseName.$PID"
|
|
|
|
Write-Step "Downloading Codex CLI"
|
|
if ($installLayout -eq "Package") {
|
|
Invoke-WebRequest -Uri $checksumMetadata.Url -OutFile $checksumPath
|
|
Test-ArchiveDigest -ArchivePath $checksumPath -ExpectedDigest $checksumMetadata.Sha256
|
|
$expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset
|
|
} else {
|
|
$expectedPackageDigest = $packageMetadata.Sha256
|
|
}
|
|
Invoke-WebRequest -Uri $packageMetadata.Url -OutFile $archivePath
|
|
Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $expectedPackageDigest
|
|
|
|
New-Item -ItemType Directory -Force -Path $releasesDir | Out-Null
|
|
if (Test-Path -LiteralPath $stagingDir) {
|
|
Remove-Item -LiteralPath $stagingDir -Recurse -Force
|
|
}
|
|
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
|
|
if ($installLayout -eq "Package") {
|
|
tar -xzf $archivePath -C $stagingDir
|
|
if (-not (Test-PackageContentsAreComplete -PackageDir $stagingDir)) {
|
|
throw "Downloaded Codex package archive did not contain the expected package layout."
|
|
}
|
|
} else {
|
|
$extractDir = Join-Path $tempDir "extract"
|
|
New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
|
|
tar -xzf $archivePath -C $extractDir
|
|
|
|
$vendorRoot = Join-Path $extractDir "package/vendor/$target"
|
|
$resourcesDir = Join-Path $stagingDir "codex-resources"
|
|
New-Item -ItemType Directory -Force -Path $resourcesDir | Out-Null
|
|
$copyMap = @{
|
|
"codex/codex.exe" = "codex.exe"
|
|
"codex/codex-command-runner.exe" = "codex-resources\codex-command-runner.exe"
|
|
"codex/codex-windows-sandbox-setup.exe" = "codex-resources\codex-windows-sandbox-setup.exe"
|
|
"path/rg.exe" = "codex-resources\rg.exe"
|
|
}
|
|
|
|
foreach ($relativeSource in $copyMap.Keys) {
|
|
Copy-Item -LiteralPath (Join-Path $vendorRoot $relativeSource) -Destination (Join-Path $stagingDir $copyMap[$relativeSource])
|
|
}
|
|
|
|
if (-not (Test-LegacyPlatformNpmContentsAreComplete -PackageDir $stagingDir)) {
|
|
throw "Downloaded Codex npm archive did not contain the expected legacy platform package layout."
|
|
}
|
|
}
|
|
|
|
if (Test-Path -LiteralPath $releaseDir) {
|
|
Remove-Item -LiteralPath $releaseDir -Recurse -Force
|
|
}
|
|
Move-Item -LiteralPath $stagingDir -Destination $releaseDir
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $standaloneRoot | Out-Null
|
|
Ensure-Junction -LinkPath $currentDir -TargetPath $releaseDir -InstallerOwnedTargetPrefix $releasesDir
|
|
|
|
$visibleParent = Split-Path -Parent $visibleBinDir
|
|
$currentBinDir = if ($installLayout -eq "Package") {
|
|
Join-Path $currentDir "bin"
|
|
} else {
|
|
$currentDir
|
|
}
|
|
New-Item -ItemType Directory -Force -Path $visibleParent | Out-Null
|
|
$oldStandaloneBackup = Move-OldStandaloneBinIfApproved -VisibleBinDir $visibleBinDir -DefaultVisibleBinDir $defaultVisibleBinDir
|
|
try {
|
|
Ensure-Junction -LinkPath $visibleBinDir -TargetPath $currentBinDir -InstallerOwnedTargetPrefix $standaloneRoot
|
|
Test-VisibleCodexCommand -VisibleBinDir $visibleBinDir
|
|
} catch {
|
|
if ($null -ne $oldStandaloneBackup -and (Test-Path -LiteralPath $oldStandaloneBackup)) {
|
|
if (Test-Path -LiteralPath $visibleBinDir) {
|
|
Remove-Item -LiteralPath $visibleBinDir -Recurse -Force
|
|
}
|
|
Move-Item -LiteralPath $oldStandaloneBackup -Destination $visibleBinDir
|
|
}
|
|
throw
|
|
}
|
|
if ($null -ne $oldStandaloneBackup) {
|
|
Remove-Item -LiteralPath $oldStandaloneBackup -Recurse -Force
|
|
}
|
|
}
|
|
} finally {
|
|
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Maybe-HandleConflictingInstall -Conflict $conflictingInstall
|
|
|
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
$prioritizeVisibleBin = $null -ne $conflictingInstall
|
|
if ($prioritizeVisibleBin) {
|
|
$newUserPath = Prepend-PathEntry -PathValue $userPath -Entry $visibleBinDir
|
|
if ($newUserPath -cne $userPath) {
|
|
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
|
|
Write-Step "PATH updated for future PowerShell sessions."
|
|
} else {
|
|
Write-Step "$visibleBinDir is already first on PATH."
|
|
}
|
|
} elseif (-not (Path-Contains -PathValue $userPath -Entry $visibleBinDir)) {
|
|
if ([string]::IsNullOrWhiteSpace($userPath)) {
|
|
$newUserPath = $visibleBinDir
|
|
} else {
|
|
$newUserPath = "$visibleBinDir;$userPath"
|
|
}
|
|
|
|
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
|
|
Write-Step "PATH updated for future PowerShell sessions."
|
|
} elseif (Path-Contains -PathValue $env:Path -Entry $visibleBinDir) {
|
|
Write-Step "$visibleBinDir is already on PATH."
|
|
} else {
|
|
Write-Step "PATH is already configured for future PowerShell sessions."
|
|
}
|
|
|
|
if ($prioritizeVisibleBin) {
|
|
$env:Path = Prepend-PathEntry -PathValue $env:Path -Entry $visibleBinDir
|
|
} elseif (-not (Path-Contains -PathValue $env:Path -Entry $visibleBinDir)) {
|
|
if ([string]::IsNullOrWhiteSpace($env:Path)) {
|
|
$env:Path = $visibleBinDir
|
|
} else {
|
|
$env:Path = "$visibleBinDir;$env:Path"
|
|
}
|
|
}
|
|
|
|
Write-Step "Current PowerShell session: codex"
|
|
Write-Step "Future PowerShell windows: open a new PowerShell window and run: codex"
|
|
Write-Host "Codex CLI $resolvedVersion installed successfully."
|
|
|
|
$codexCommand = Join-Path $visibleBinDir "codex.exe"
|
|
if (Prompt-YesNo "Start Codex now?") {
|
|
Write-Step "Launching Codex"
|
|
& $codexCommand
|
|
}
|