Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ curl -sS http://127.0.0.1:8320/healthz
dedicated ChatGPT login. It is normal for the container to report unhealthy
before device login is complete.

Windows users running Docker Desktop should use the PowerShell bootstrap and
platform notes in [`docs/windows.md`](docs/windows.md).

## API Examples

Load the wrapper bearer token into a shell variable:
Expand Down
98 changes: 98 additions & 0 deletions docs/windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Windows Docker Setup

This project runs on Windows through Docker Desktop Linux containers. It does
not use a Windows-native container image.

## Requirements

- Windows 10 or 11 with Docker Desktop.
- Docker Desktop configured for Linux containers with the WSL 2 backend.
- PowerShell 5.1 or newer.
- A dedicated Codex/ChatGPT sign-in for this project.
- Optional: `docker login ghcr.io` if the GitHub Container Registry package is
private.

Keep the repository in a normal local path such as `C:\Users\<you>\Projects`.
Avoid synced or network-backed folders for the `data/` directory if Docker file
sharing behaves oddly.

## Setup

From the repository root in PowerShell, run:

```powershell
.\scripts\setup_windows.ps1
```

The setup script:

- copies `.env.example` to `.env` if `.env` does not exist;
- creates `data\codex-home`, `data\codex-work`, and `data\secrets`;
- writes a strong wrapper bearer token to `data\secrets\proxy_api_key` as UTF-8;
- leaves existing `.env` and token files in place unless `-ForceSecret` is
used.

Then build and run locally:

```powershell
docker compose up --build -d
```

For a published image, pass the image tag once during setup:

```powershell
.\scripts\setup_windows.ps1 -Image "ghcr.io/subdepthtech/codex-cli-provider:v0.1.2"
docker compose -f docker-compose.image.yml pull
docker compose -f docker-compose.image.yml up -d
```

Do not use `latest`.

## Codex Login

Complete Codex login inside the running container:

```powershell
docker exec -it codex-cli-provider `
codex login --device-auth `
-c 'forced_login_method="chatgpt"' `
-c 'cli_auth_credentials_store="file"'
```

Credentials are written to `/root/.codex` in the Linux container, backed by the
local `data\codex-home` directory.

Confirm readiness:

```powershell
curl.exe -sS http://127.0.0.1:8320/healthz
```

## Client Settings

Use these settings from Windows apps:

- Base URL: `http://127.0.0.1:8320/v1`
- API key: the contents of `data\secrets\proxy_api_key`
- Model: `codex-cli-default`
- Concurrency: `1`

Load the wrapper token into PowerShell for manual API checks:

```powershell
$ProxyApiKey = (Get-Content -Raw data\secrets\proxy_api_key).Trim()
curl.exe -sS `
-H "Authorization: Bearer $ProxyApiKey" `
http://127.0.0.1:8320/v1/models
```

## Notes

- Keep Docker Desktop in Linux-container mode.
- Do not set `OPENAI_API_KEY`.
- Keep Codex/ChatGPT auth in `data\codex-home`.
- Keep the wrapper bearer token in `data\secrets\proxy_api_key`.
- If `docker compose` cannot mount files, check Docker Desktop file-sharing
settings for the drive that contains the repository.
- If `data\secrets\proxy_api_key` was created manually, make sure it is UTF-8
text and contains only the token plus an optional trailing newline.
106 changes: 106 additions & 0 deletions scripts/setup_windows.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
param(
[string]$Image = "",
[switch]$ForceSecret
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RepoRoot = Split-Path -Parent $ScriptDir
$Utf8NoBom = New-Object System.Text.UTF8Encoding -ArgumentList $false

function Write-Utf8File {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$Content
)
[IO.File]::WriteAllText($Path, $Content, $Utf8NoBom)
}

function Set-DotEnvValue {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][string]$Value
)

$escapedName = [regex]::Escape($Name)
if (Test-Path -LiteralPath $Path) {
$lines = New-Object "System.Collections.Generic.List[string]"
foreach ($line in [IO.File]::ReadAllLines($Path)) {
$lines.Add($line)
}
} else {
$lines = New-Object "System.Collections.Generic.List[string]"
}

$updated = $false
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match "^\s*#?\s*$escapedName=") {
$lines[$i] = "$Name=$Value"
$updated = $true
}
}

if (-not $updated) {
$lines.Add("$Name=$Value")
}

[IO.File]::WriteAllLines($Path, $lines, $Utf8NoBom)
}

Set-Location $RepoRoot

$envExamplePath = Join-Path $RepoRoot ".env.example"
$envPath = Join-Path $RepoRoot ".env"
$codexHomePath = Join-Path $RepoRoot "data/codex-home"
$codexWorkPath = Join-Path $RepoRoot "data/codex-work"
$secretsPath = Join-Path $RepoRoot "data/secrets"
$proxySecretPath = Join-Path $secretsPath "proxy_api_key"

if (-not (Test-Path -LiteralPath $envExamplePath)) {
throw "Missing .env.example in $RepoRoot"
}

if (-not (Test-Path -LiteralPath $envPath)) {
Copy-Item -LiteralPath $envExamplePath -Destination $envPath
Write-Host "Created .env from .env.example"
} else {
Write-Host "Keeping existing .env"
}

if ($Image.Trim()) {
Set-DotEnvValue -Path $envPath -Name "CODEX_CLI_PROVIDER_IMAGE" -Value $Image.Trim()
Write-Host "Set CODEX_CLI_PROVIDER_IMAGE in .env"
}

New-Item -ItemType Directory -Force -Path $codexHomePath, $codexWorkPath, $secretsPath | Out-Null
Write-Host "Ensured data/codex-home, data/codex-work, and data/secrets exist"

if ((Test-Path -LiteralPath $proxySecretPath) -and (-not $ForceSecret)) {
Write-Host "Keeping existing data/secrets/proxy_api_key"
} else {
$tokenBytes = New-Object byte[] 48
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
try {
$rng.GetBytes($tokenBytes)
} finally {
$rng.Dispose()
}
$token = [Convert]::ToBase64String($tokenBytes)
Write-Utf8File -Path $proxySecretPath -Content "$token`n"
Write-Host "Wrote new UTF-8 wrapper bearer token to data/secrets/proxy_api_key"
}

Write-Host ""
Write-Host "Next steps:"
if ($Image.Trim()) {
Write-Host " docker compose -f docker-compose.image.yml pull"
Write-Host " docker compose -f docker-compose.image.yml up -d"
} else {
Write-Host " docker compose up --build -d"
}
Write-Host " docker exec -it codex-cli-provider codex login --device-auth -c 'forced_login_method=`"chatgpt`"' -c 'cli_auth_credentials_store=`"file`"'"
Write-Host ""
Write-Host "Use http://127.0.0.1:8320/v1 as the client base URL."
Loading