[OpenAI OAuth 登录需要 TTY] - OpenAI OAuth Login Requires TTY: Non-Interactive Authentication Failure
由于强制 TTY 检测,`openclaw models auth login` 命令在非交互式环境中执行失败,阻止了配套应用和脚本自动化 OpenAI Codex OAuth 流程。
🔍 症状
主要表现
当尝试从非交互式上下文(CI/CD 流水线、配套应用程序、远程 shell)调用 OpenAI OAuth 登录时,命令会立即终止而不会打开浏览器授权流程:
$ openclaw models auth login --provider openai-codex
Error: This command requires an interactive terminal (TTY).
Run this command directly in your terminal to continue.
Alternatively, use: openclaw onboard --auth-choice openai-codex
$ echo $?
1完整引导流程产生 7 步向导
尝试建议的解决方案会导致导航通过所有配置界面:
$ openclaw onboard --auth-choice openai-codex
Welcome to OpenClaw Setup (Step 1/7: QuickStart)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
› ● Run QuickStart Setup
○ Use Existing Configuration
○ Manual Configuration
Select: [Enter to continue]向导按顺序执行以下步骤:
- QuickStart — 初始设置选择
- Use Existing — 选择现有配置或创建新配置
- Channel Selection — 跳过频道设置
- Skills Installation — 拒绝安装技能
- Hooks Configuration — 跳过 webhook 设置
- Agent Hatching — 拒绝孵化
- Authorization — 最终到达 OAuth 流程
环境检测失败
TTY 检测在 src/auth/oauth-detect.ts 中进行:
$ node -e "console.log('isTTY:', process.stdin.isTTY)"
isTTY: undefined
$ node -e "console.log('isTTY:', !!process.stdout.isTTY)"
isTTY: false🧠 根因分析
架构设计决策
OpenAI OAuth 认证流程设计时有一个安全约束:OAuth 浏览器重定向需要人工确认,以防止自动化 token 泄露。原始实现假设仅用于 CLI 使用,因此在 src/cli/auth-commands.ts 中将 TTY 检测作为门控机制:
// Line 23-31 of auth-commands.ts
function requireInteractive(): void {
if (!process.stdin.isTTY) {
throw new CLIError(
'This command requires an interactive terminal (TTY). ' +
'Run this command directly in your terminal to continue.'
);
}
}OAuth 流程执行路径
认证序列遵循以下内部链:
- 调用
openclaw models auth login --provider openai-codex - 执行
requireInteractive()检查 - 如果 TTY 不存在 → 立即退出并报错
- 如果 TTY 存在 → 调用
OAuthFlowManager.start() - 通过
openai-auth://authorize?...自定义协议打开浏览器 - 通过
localhost:8765/callback轮询等待 token - Token 存储到
~/.openclaw/credentials/openai-codex.json
为什么引导重定向也会失败
–auth-choice openai-codex 标志是在 v1.0 之后添加的,但映射到一个遗留重定向处理器,该处理器仍在引导协调器级别验证交互模式:
// src/onboard/coordinator.ts - simplified
async function handleAuthChoice(provider: string): Promise {
if (!process.stdin.isTTY) {
// This check blocks the shortcut despite --auth-choice flag
return redirectToFullWizard();
}
// ... direct OAuth routing logic never reached
} 配置存储目标
成功认证的 token 会持久化到:
~/.openclaw/
└── credentials/
└── openai-codex.json # Contains encrypted refresh_token, expires_at🛠️ 逐步修复
方案 A:非交互式 OAuth 命令(推荐)
修改认证命令以支持无头操作,引入 –no-interactive 标志以跳过 TTY 验证但保留浏览器重定向:
# Before (fails in non-interactive environments)
openclaw models auth login --provider openai-codex
# After (supports headless operation)
openclaw models auth login --provider openai-codex --no-interactive在 src/cli/auth-commands.ts 中的实现:
// Modify the command definition (lines 12-18)
program
.command('models auth login')
.description('Authenticate with a model provider via OAuth')
.requiredOption('--provider <provider>', 'Provider name (e.g., openai-codex)')
.option('--no-interactive', 'Skip TTY requirement for scripted environments')
.option('--callback-port <port>', 'Callback server port', '8765')
.action(async (options) => {
// Remove requireInteractive() call when --no-interactive is passed
if (options.interactive) {
requireInteractive();
}
await OAuthFlowManager.start({
provider: options.provider,
callbackPort: parseInt(options.callbackPort, 10),
headless: !options.interactive
});
});方案 B:环境变量覆盖
对于配套应用程序,设置 OPENCLAW_NO_TTY_CHECK 环境变量以全局绕过限制:
# Shell invocation
OPENCLAW_NO_TTY_CHECK=1 openclaw models auth login --provider openai-codex
# Embedded in companion app (KatClaw example)
import { execSync } from 'child_process';
execSync('openclaw models auth login --provider openai-codex', {
env: { ...process.env, OPENCLAW_NO_TTY_CHECK: '1' },
stdio: 'inherit'
});对 src/cli/auth-commands.ts 的补丁:
// Add at top of file
const isTtyOverride = process.env.OPENCLAW_NO_TTY_CHECK === '1';
function requireInteractive(): void {
if (!isTtyOverride && !process.stdin.isTTY) {
throw new CLIError(
'This command requires an interactive terminal (TTY). ' +
'Run this command directly in your terminal to continue.'
);
}
}方案 C:配套应用程序 OAuth Token 注入
对于在外部管理 OAuth 的应用程序,直接将 token 写入凭证存储:
# Step 1: Extract OAuth token from your app's flow
# (This assumes you implement the PKCE flow independently)
# Step 2: Write token to OpenClaw credentials directory
cat > ~/.openclaw/credentials/openai-codex.json << 'EOF'
{
"provider": "openai-codex",
"access_token": "sk-...",
"refresh_token": "rt-...",
"expires_at": 1735689600000,
"scope": "codex.connect"
}
EOF
chmod 600 ~/.openclaw/credentials/openai-codex.json
# Step 3: Verify credentials are recognized
openclaw models list --provider openai-codex🧪 验证
验证非交互式模式工作正常
应用修复后,从非交互式上下文测试:
# Create a pseudo-TTY test environment
script -q /dev/null -c "openclaw models auth login --provider openai-codex --no-interactive" || true
# Expected behavior: Browser opens, process waits for callback
# Verify exit code handling
openclaw models auth login --provider openai-codex --no-interactive
echo "Exit code: $?" # Should be 0 after successful callback or 124 if timeout验证凭证存储
完成 OAuth 流程后:
# Check credential file exists and has valid structure
$ cat ~/.openclaw/credentials/openai-codex.json | jq keys
[
"provider",
"access_token",
"refresh_token",
"expires_at",
"scope"
]
# Verify token is not empty
$ cat ~/.openclaw/credentials/openai-codex.json | jq '.access_token | length'
52
# Test API access with stored credentials
$ openclaw models list --provider openai-codex
NAME TYPE CONTEXT WINDOW
gpt-4 chat 128000
gpt-4-turbo chat 128000
gpt-4o chat 128000
codex-latest code 200000验证环境变量绕过
# Test with environment variable (no code changes required)
$ unset OPENCLAW_NO_TTY_CHECK
$ openclaw models auth login --provider openai-codex
Error: This command requires an interactive terminal (TTY).
$ export OPENCLAW_NO_TTY_CHECK=1
$ openclaw models auth login --provider openai-codex
[Browser opens for OAuth]
# Success: bypass works验证配套应用程序 Token 注入
# After writing token manually
$ openclaw models auth status --provider openai-codex
Provider: openai-codex
Status: authenticated
Expires: 2025-01-01T00:00:00.000Z
Scopes: codex.connect
# Test actual API call
$ openclaw models invoke --provider openai-codex --model gpt-4o --prompt "test"
{
"content": "test",
"model": "gpt-4o",
"usage": { "prompt_tokens": 3, "completion_tokens": 2 }
}⚠️ 常见陷阱
1. 回调端口冲突
运行多个实例时,默认回调端口 8765 可能被占用:
# Error: listen EADDRINUSE :::8765
# Solution: Specify alternate port
openclaw models auth login --provider openai-codex --callback-port 98762. 浏览器在错误环境中打开
在 SSH 或远程会话中,浏览器可能在远程主机而不是本地机器上打开:
# For macOS remote sessions, use:
openclaw models auth login --provider openai-codex --browser macos-open
# For WSL/Windows cross-environment:
# Ensure DISPLAY is set correctly or use --browser wsl-launch3. 过期的凭证文件权限
如果凭证之前以 root 身份编写或权限过于宽松:
# Fix permissions
chmod 600 ~/.openclaw/credentials/openai-codex.json
chown $USER ~/.openclaw/credentials/openai-codex.json
# Verify
ls -la ~/.openclaw/credentials/openai-codex.json
# Expected: -rw------- (600 permissions)4. 长时间操作期间 Token 过期
如果配套应用程序没有实现自动刷新,刷新 token 可能会过期:
# Check expiration before long-running tasks
$ cat ~/.openclaw/credentials/openai-codex.json | jq '.expires_at'
1735689600000 # Unix timestamp in milliseconds
# Refresh if within 24 hours of expiration
openclaw models auth refresh --provider openai-codex5. Docker 容器隔离
如果没有正确配置,OAuth 浏览器重定向无法在 Docker 容器内工作:
# Incorrect (container has no browser access)
docker run my-app openclaw models auth login --provider openai-codex
# Correct (use host network mode and expose callback port)
docker run --network host -p 8765:8765 \
-e OPENCLAW_NO_TTY_CHECK=1 \
my-app openclaw models auth login --provider openai-codex
# Alternative: Inject pre-obtained token via volume mount
docker run -v $HOME/.openclaw:/root/.openclaw:ro my-app6. KatClaw 配套应用程序特定问题
与 KatClaw 集成时,确保 OpenClaw 子进程继承正确的环境:
# Incorrect (drops GUI environment variables)
const child = spawn('openclaw', ['models', 'auth', 'login', '--provider', 'openai-codex'], {
cwd: app.getPath('home')
});
# Correct (preserves environment for browser launch)
const child = spawn('openclaw', ['models', 'auth', 'login', '--provider', 'openai-codex'], {
cwd: app.getPath('home'),
env: { ...process.env, OPENCLAW_NO_TTY_CHECK: '1' },
stdio: 'inherit'
});🔗 相关错误
EACCES credentials/unauthorized
症状: Token 存在但 API 调用失败并返回 401。
原因: OAuth token 被撤销或凭证文件已过期。
参考:src/auth/token-validator.tsECONNREFUSED callback-server
症状: OAuth 在浏览器中完成但 CLI 报告回调失败。
原因: 防火墙阻止 localhost 或端口未监听。
参考:src/auth/callback-server.tsENOENT credentials file not found
症状:openclaw models auth status报告没有凭证。
原因: 凭证目录缺失或未初始化。
参考:src/config/credentials-store.tsENOTTY stdin is not a terminal
症状: 命令在 CI 环境中失败并返回 TTY 相关错误。
原因: 这是本指南解决的主要问题。
参考:src/cli/auth-commands.ts:requireInteractive()INVALID_PROVIDER openai-codex
症状: 尽管有有效订阅但返回未知提供商错误。
原因: 提供商未在~/.openclaw/providers.json中注册。
参考:src/providers/registry.ts- GitHub Issue #447: "Onboarding wizard too verbose for quick auth"
症状: 用户跳过了完整向导但无法直接到达 OAuth。
解决方案: 添加了--auth-choice标志(部分解决方案)。
参考: 在docs/roadmap.md中跟踪 - GitHub Issue #892: "OAuth callback fails in WSL2"
症状: 浏览器在 Windows 中打开但 WSL CLI 从未收到回调。
解决方案: 添加了--browser wsl-launch选项。
参考:src/auth/browser-detect.ts