Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,8 @@ export class CodexAgentService implements AgentService {
// /proc/<pid>/cmdline 会把完整对话历史(含跨 thread/猫/用户内容)暴露给任何
// 并发进程。'--' 结束选项解析,'-' 让 codex 从 stdin 读取 PROMPT。
const promptArgs = ['--', '-'];
// Clowder owns per-invocation config; stale user config.toml must not break backend calls.
const ignoreUserConfigArgs = ['--ignore-user-config'];

// Dedup: skip system --config/--flag pairs that the user explicitly overrides (#567).
const dedup = (src: string[]): string[] => {
Expand All @@ -884,6 +886,7 @@ export class CodexAgentService implements AgentService {
'resume',
options.sessionId,
'--json',
...ignoreUserConfigArgs,
...dedup(modelArgs),
...dedup(reasoningArgs),
...dedup(contextWindowArgs),
Expand All @@ -900,6 +903,7 @@ export class CodexAgentService implements AgentService {
: [
'exec',
'--json',
...ignoreUserConfigArgs,
...dedup(modelArgs),
...dedup(reasoningArgs),
...dedup(contextWindowArgs),
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/utils/cli-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ export function resolveCliCommand(command: string, opts?: { skipPathProbe?: bool
if (appData) winDirs.push(resolve(appData, 'npm'));
if (localAppData) winDirs.push(resolve(localAppData, 'npm'));
if (command === 'agy' && localAppData) winDirs.push(resolve(localAppData, 'agy', 'bin'));
// Codex's native Windows desktop app (OpenAI Codex installer) puts codex.exe
// under %LOCALAPPDATA%\OpenAI\Codex\bin, which is never added to PATH.
if (command === 'codex' && localAppData) winDirs.push(resolve(localAppData, 'OpenAI', 'Codex', 'bin'));
for (const dir of winDirs) {
// Prefer .cmd shim (more reliable for resolveWindowsShimSpawn)
const cmdCandidate = resolve(dir, `${command}.cmd`);
Expand Down
30 changes: 30 additions & 0 deletions packages/api/test/cli-resolve.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,36 @@ test(
},
);

test(
'resolveCliCommand finds codex in LOCALAPPDATA/OpenAI/Codex/bin on Windows',
{ skip: process.platform !== 'win32' && 'Windows-only (Codex native desktop app fallback)' },
() => {
const tempRoot = mkdtempSync(join(tmpdir(), 'cli-resolve-codex-localappdata-'));
const codexDir = join(tempRoot, 'local', 'OpenAI', 'Codex', 'bin');
mkdirSync(codexDir, { recursive: true });

const fakeCodex = join(codexDir, 'codex.exe');
writeFileSync(fakeCodex, 'MZ', 'utf8');

const originalAppData = process.env.APPDATA;
const originalLocalAppData = process.env.LOCALAPPDATA;
try {
process.env.APPDATA = join(tempRoot, 'roaming');
process.env.LOCALAPPDATA = join(tempRoot, 'local');
invalidateCliCommand('codex');
const result = resolveCliCommand('codex');
assert.equal(result, fakeCodex, 'should find official Windows Codex native desktop app binary path');
} finally {
invalidateCliCommand('codex');
if (originalAppData === undefined) delete process.env.APPDATA;
else process.env.APPDATA = originalAppData;
if (originalLocalAppData === undefined) delete process.env.LOCALAPPDATA;
else process.env.LOCALAPPDATA = originalLocalAppData;
rmSync(tempRoot, { recursive: true, force: true });
}
},
);

// --- Unix HOME fallback ---

test(
Expand Down
3 changes: 3 additions & 0 deletions packages/api/test/codex-agent-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ describe('CodexAgentService Tests (CLI mode)', { concurrency: false }, () => {
]);

const msgs = await promise;
const args = spawnFn.mock.calls[0].arguments[1];

assert.equal(msgs.length, 3);
assert.equal(msgs[0].type, 'session_init');
Expand All @@ -272,6 +273,7 @@ describe('CodexAgentService Tests (CLI mode)', { concurrency: false }, () => {
assert.equal(msgs[1].type, 'text');
assert.equal(msgs[1].content, 'Hello from Codex!');
assert.equal(msgs[2].type, 'done');
assert.ok(args.includes('--ignore-user-config'), 'codex invocations must ignore stale user config.toml');
});

test('uses exec resume when sessionId is provided', async () => {
Expand Down Expand Up @@ -299,6 +301,7 @@ describe('CodexAgentService Tests (CLI mode)', { concurrency: false }, () => {
// resume 子命令不接受 --sandbox;sandbox mode is replayed through --config.
assert.ok(!args.includes('--sandbox'), 'resume args must not include --sandbox');
assert.ok(args.includes('--json'), 'resume args must include --json');
assert.ok(args.includes('--ignore-user-config'), 'resume args must ignore stale user config.toml');
const modelFlagIndex = args.indexOf('--model');
assert.ok(modelFlagIndex >= 0, 'resume args must include --model');
assert.equal(args[modelFlagIndex + 1], 'gpt-5.3-codex');
Expand Down
82 changes: 82 additions & 0 deletions scripts/services/prereq-check.proxy-env.test.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
$ErrorActionPreference = "Stop"

. "$PSScriptRoot\prereq-check.ps1"

function Save-EnvVars {
param([string[]]$Names)
$saved = @{}
foreach ($name in $Names) {
$saved[$name] = [Environment]::GetEnvironmentVariable($name, "Process")
}
return $saved
}

function Restore-EnvVars {
param(
[hashtable]$Saved,
[string[]]$Names
)
foreach ($name in $Names) {
[Environment]::SetEnvironmentVariable($name, $Saved[$name], "Process")
}
}

$names = @(
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"HF_HUB_DISABLE_SYMLINKS",
"HF_HUB_DISABLE_SYMLINKS_WARNING"
)
$saved = Save-EnvVars -Names $names
$savedTransportMode = $script:CatCafeHfDownloadTransportMode

$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("cat-cafe-prereq-test-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
$fakePython = Join-Path $tempDir "python.cmd"
$capture = Join-Path $tempDir "env.txt"

try {
Set-Content -LiteralPath $fakePython -Encoding ASCII -Value @"
@echo off
(
echo HTTP_PROXY=%HTTP_PROXY%
echo HTTPS_PROXY=%HTTPS_PROXY%
echo ALL_PROXY=%ALL_PROXY%
echo HF_HUB_DISABLE_SYMLINKS=%HF_HUB_DISABLE_SYMLINKS%
echo HF_HUB_DISABLE_SYMLINKS_WARNING=%HF_HUB_DISABLE_SYMLINKS_WARNING%
) > "%CAT_CAFE_TEST_ENV_CAPTURE%"
exit /b 0
"@

$env:CAT_CAFE_TEST_ENV_CAPTURE = $capture
$env:HTTP_PROXY = "http://127.0.0.1:9"
$env:HTTPS_PROXY = "http://127.0.0.1:9"
$env:ALL_PROXY = "http://127.0.0.1:9"
$script:CatCafeHfDownloadTransportMode = "direct"
Remove-Item Env:HF_HUB_DISABLE_SYMLINKS,Env:HF_HUB_DISABLE_SYMLINKS_WARNING -ErrorAction SilentlyContinue

Invoke-ModelDownloadWithRetry -VenvPython $fakePython -ModelId "dummy/model" -Loader "snapshot"

$captured = Get-Content -LiteralPath $capture
foreach ($line in @("HTTP_PROXY=", "HTTPS_PROXY=", "ALL_PROXY=")) {
if ($captured -notcontains $line) {
throw "Expected child process proxy env to be cleared; missing '$line'. Captured: $($captured -join '; ')"
}
}
foreach ($line in @("HF_HUB_DISABLE_SYMLINKS=1", "HF_HUB_DISABLE_SYMLINKS_WARNING=1")) {
if ($captured -notcontains $line) {
throw "Expected Windows HuggingFace symlink guard '$line'. Captured: $($captured -join '; ')"
}
}
if ($env:HTTP_PROXY -ne "http://127.0.0.1:9") {
throw "Expected parent HTTP_PROXY to be restored after child invocation."
}

Write-Host "prereq-check.proxy-env.test.ps1: PASS"
} finally {
Remove-Item Env:CAT_CAFE_TEST_ENV_CAPTURE -ErrorAction SilentlyContinue
$script:CatCafeHfDownloadTransportMode = $savedTransportMode
Restore-EnvVars -Saved $saved -Names $names
Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
123 changes: 91 additions & 32 deletions scripts/services/prereq-check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -155,17 +155,44 @@ function Test-SourceMode {
# "corp-proxy reaches Tsinghua even though it can't reach pypi" case:
# Sync-SystemProxy no longer gates the candidate on a single pypi probe,
# so per-source decisions in Assert-Network always see the candidate.
param([string]$Url, [int]$TimeoutSec = 5, [string]$CandidateProxy = $null)
param(
[string]$Url,
[int]$TimeoutSec = 5,
[string]$CandidateProxy = $null,
[ValidateSet("HEAD", "GET")][string]$Method = "HEAD"
)

$probe = {
param($Proxy)
$resp = $null
$stream = $null
try {
$req = [System.Net.HttpWebRequest]::Create($Url)
$req.Proxy = $Proxy
$req.Method = $Method
$req.Timeout = $TimeoutSec * 1000
$req.ReadWriteTimeout = $TimeoutSec * 1000
$req.AllowAutoRedirect = $true
$req.UserAgent = "cat-cafe-prereq-check"
$resp = $req.GetResponse()
if ($Method -eq "GET") {
$stream = $resp.GetResponseStream()
$buffer = New-Object byte[] 8192
while ($stream -and $stream.Read($buffer, 0, $buffer.Length) -gt 0) {}
}
return $true
} catch {
return $false
} finally {
if ($stream) { $stream.Dispose() }
if ($resp) { $resp.Close() }
}
}

# 1. Try without any proxy at all.
try {
$req = [System.Net.HttpWebRequest]::Create($Url)
$req.Proxy = $null
$req.Method = 'HEAD'
$req.Timeout = $TimeoutSec * 1000
$resp = $req.GetResponse()
$resp.Close()
if (& $probe $null) {
return 'direct'
} catch {}
}
# 2. Try via candidate proxy (anonymous -- DON'T let .NET auto-fill the
# SSPI token, that would mask auth-required corp proxies as reachable).
$proxyUrl = $CandidateProxy
Expand All @@ -175,18 +202,12 @@ function Test-SourceMode {
if (-not $proxyUrl) { $proxyUrl = $env:HTTP_PROXY }
}
if ($proxyUrl) {
try {
$webProxy = New-Object System.Net.WebProxy($proxyUrl)
$webProxy.UseDefaultCredentials = $false
$webProxy.Credentials = $null
$req = [System.Net.HttpWebRequest]::Create($Url)
$req.Proxy = $webProxy
$req.Method = 'HEAD'
$req.Timeout = $TimeoutSec * 1000
$resp = $req.GetResponse()
$resp.Close()
$webProxy = New-Object System.Net.WebProxy($proxyUrl)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wrap proxy construction in the probe try/catch

When HTTPS_PROXY/HTTP_PROXY or the registry-derived candidate is malformed, New-Object System.Net.WebProxy($proxyUrl) can throw before the $probe catch block runs; Windows proxy settings commonly use per-scheme strings like http=host:port;https=host:port, which Get-SystemProxyCandidate currently turns into a non-URI. Before this refactor the constructor was inside the try/catch and the source was classified as unreachable, but now one bad proxy value aborts the whole prerequisite check instead of falling back/giving guidance.

Useful? React with 👍 / 👎.

$webProxy.UseDefaultCredentials = $false
$webProxy.Credentials = $null
if (& $probe $webProxy) {
return 'proxy'
} catch {}
}
}
return 'unreachable'
}
Expand Down Expand Up @@ -242,6 +263,11 @@ function Invoke-ModelDownloadWithRetry {
[string]$Loader = "snapshot"
)

if ($env:OS -eq "Windows_NT") {
$env:HF_HUB_DISABLE_SYMLINKS = if ($env:HF_HUB_DISABLE_SYMLINKS) { $env:HF_HUB_DISABLE_SYMLINKS } else { "1" }
$env:HF_HUB_DISABLE_SYMLINKS_WARNING = if ($env:HF_HUB_DISABLE_SYMLINKS_WARNING) { $env:HF_HUB_DISABLE_SYMLINKS_WARNING } else { "1" }
}

$script = switch ($Loader) {
"snapshot" { @"
import sys, time, os
Expand Down Expand Up @@ -362,8 +388,34 @@ sys.exit(1)
default { throw "Invoke-ModelDownloadWithRetry: unknown loader '$Loader'" }
}

& $VenvPython -c $script $ModelId
if ($LASTEXITCODE -ne 0) { throw "Failed to download model: $ModelId" }
$proxyVarNames = if ($env:OS -eq "Windows_NT") {
@("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY")
} else {
@("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy")
}
$savedProxyEnv = @{}
$clearProxyForDownload = ($script:CatCafeHfDownloadTransportMode -eq "direct")
$exitCode = $null
try {
if ($clearProxyForDownload) {
Comment on lines +401 to +404

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect configured HF endpoints before clearing proxies

When a user sets HF_ENDPOINT/HF_HUB_ENDPOINT to a mirror that is only reachable through their proxy, this branch still clears the proxy for the Python child whenever the hard-coded huggingface.co/.../config.json probe was direct. snapshot_download honors the configured endpoint, so it will go to that mirror with HTTP_PROXY/HTTPS_PROXY removed and fail even though the user's proxy config was valid. Please base CatCafeHfDownloadTransportMode on the effective HF endpoint or avoid clearing proxies when one is configured.

Useful? React with 👍 / 👎.

# HuggingFace artifacts can redirect to CDN/CAS hosts that are
# not covered by NO_PROXY. When Assert-Network proved artifact
# downloads work direct, keep this Python child direct-only.
foreach ($name in $proxyVarNames) {
$savedProxyEnv[$name] = [Environment]::GetEnvironmentVariable($name, "Process")
[Environment]::SetEnvironmentVariable($name, $null, "Process")
}
}
& $VenvPython -c $script $ModelId
$exitCode = $LASTEXITCODE
} finally {
if ($clearProxyForDownload) {
foreach ($name in $proxyVarNames) {
[Environment]::SetEnvironmentVariable($name, $savedProxyEnv[$name], "Process")
}
}
}
if ($exitCode -ne 0) { throw "Failed to download model: $ModelId" }
}

function Assert-Network {
Expand All @@ -374,6 +426,7 @@ function Assert-Network {
# decisions don't depend on whether env was already set.
$candidate = Get-SystemProxyCandidate
$needProxyInjection = $false
$script:CatCafeHfDownloadTransportMode = $null

# FIRST: classify the user's PIP_INDEX_URL (if set) the same way we
# classify public mirrors. Internal corporate mirrors typically
Expand Down Expand Up @@ -455,26 +508,32 @@ function Assert-Network {
Write-Host " Auto-set PIP_INDEX_URL = Tsinghua mirror (primary)"
}

# Same two-mode probe for HuggingFace.
$hfMode = Test-SourceMode -Url "https://huggingface.co" -TimeoutSec 5 -CandidateProxy $candidate
# Same two-mode probe for HuggingFace. Probe a real model artifact rather
# than the homepage: some networks allow API/root requests but break TLS on
# /resolve artifact downloads, which is the path snapshot_download needs.
$hfProbeUrl = "https://huggingface.co/BAAI/bge-small-zh-v1.5/resolve/main/config.json"
$hfMode = Test-SourceMode -Url $hfProbeUrl -TimeoutSec 10 -CandidateProxy $candidate -Method "GET"
if ($hfMode -eq 'direct') {
Write-Host " HuggingFace connectivity [OK] (direct)"
Add-NoProxyHost "huggingface.co"
Write-Host " HuggingFace artifact download [OK] (direct)"
$script:CatCafeHfDownloadTransportMode = "direct"
} elseif ($hfMode -eq 'proxy') {
Write-Host " HuggingFace connectivity [OK] (via proxy: $candidate)"
Write-Host " HuggingFace artifact download [OK] (via proxy: $candidate)"
$script:CatCafeHfDownloadTransportMode = "proxy"
$needProxyInjection = $true
} else {
$hfMirrorMode = Test-SourceMode -Url "https://hf-mirror.com" -TimeoutSec 5 -CandidateProxy $candidate
$hfMirrorProbeUrl = "https://hf-mirror.com/BAAI/bge-small-zh-v1.5/resolve/main/config.json"
$hfMirrorMode = Test-SourceMode -Url $hfMirrorProbeUrl -TimeoutSec 10 -CandidateProxy $candidate -Method "GET"
if ($hfMirrorMode -eq 'direct') {
Write-Host " HuggingFace unreachable, switching to hf-mirror.com (direct)"
Write-Host " HuggingFace artifact download unreachable, switching to hf-mirror.com (direct)"
$env:HF_ENDPOINT = "https://hf-mirror.com"
Add-NoProxyHost "hf-mirror.com"
$script:CatCafeHfDownloadTransportMode = "direct"
} elseif ($hfMirrorMode -eq 'proxy') {
Write-Host " HuggingFace unreachable, switching to hf-mirror.com (via proxy: $candidate)"
Write-Host " HuggingFace artifact download unreachable, switching to hf-mirror.com (via proxy: $candidate)"
$env:HF_ENDPOINT = "https://hf-mirror.com"
$script:CatCafeHfDownloadTransportMode = "proxy"
$needProxyInjection = $true
} else {
Write-ProxyGuidance -Context "huggingface.co and hf-mirror.com are unreachable in both direct and via-proxy modes; model download will definitely fail."
Write-ProxyGuidance -Context "huggingface.co and hf-mirror.com artifact downloads are unreachable in both direct and via-proxy modes; model download will definitely fail."
}
}

Expand Down
Loading
Loading