From d273978e71785871efa849ab0dd127d2433b365c Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 6 Jun 2026 19:50:19 +1000 Subject: [PATCH] Remove interactive model setup --- README.md | 16 +++--- package.json | 6 +-- pnpm-lock.yaml | 15 ------ src/public/setup.html | 63 ++++++++-------------- src/public/styles.css | 42 --------------- src/server.js | 123 ++++++------------------------------------ 6 files changed, 45 insertions(+), 220 deletions(-) diff --git a/README.md b/README.md index 12a40a7..c7b5044 100644 --- a/README.md +++ b/README.md @@ -16,22 +16,20 @@ - The container runs a wrapper web server. - The wrapper protects `/setup` with `SETUP_PASSWORD`. -- During setup, the wrapper runs `openclaw onboard ...` inside the container, writes state to the volume, and then starts the gateway. API-key providers use non-interactive setup; ChatGPT/Codex and xAI/Grok device pairing use OpenClaw's interactive device-code flows so the login URL and code can be streamed to `/setup`. +- During setup, the wrapper runs `openclaw onboard ...` inside the container, writes state to the volume, and then starts the gateway. API-key providers use non-interactive setup. Interactive device-code logins (ChatGPT/Codex, xAI/Grok) can't run from the web wizard — complete those by running `openclaw wizard` in the Railway console. - After setup, **`/` is OpenClaw**. The wrapper reverse-proxies all traffic (including WebSockets) to the local gateway process. ## Getting chat tokens (so you don't have to scramble) -### ChatGPT / OpenAI Codex login +### ChatGPT / OpenAI Codex and Grok / xAI subscription login -In `/setup`, choose **OpenAI → OpenAI Codex device pairing**. Setup will stream a ChatGPT/Codex URL and a short device code; open the URL in your browser, enter the code, and keep the setup page open until OpenClaw finishes saving the OAuth profile. +These providers use an interactive device-code login that the web `/setup` wizard can't drive. Complete them from the Railway console instead: -This uses OpenClaw's `openai-codex-device-code` onboarding flow, so you do not need to paste an OpenAI API key. +1. Open your service in Railway and launch the **console** (a shell inside the running container). +2. Run `openclaw wizard` and follow the prompts. Choose the ChatGPT/Codex or xAI/Grok device login, open the URL it prints in your browser, and enter the short code. +3. Once the wizard finishes saving the OAuth profile, return to `/setup` — the instance will be configured and the gateway will start. -### Grok / xAI subscription login - -In `/setup`, choose **xAI (Grok) → xAI device code**. Setup will stream an xAI URL and a short device code; open the URL in your browser, enter the code, and keep the setup page open until OpenClaw finishes saving the OAuth profile. - -This uses OpenClaw's `xai-device-code` onboarding flow, so you can use an eligible SuperGrok or X Premium subscription without pasting an xAI API key. +This lets you use a ChatGPT/Codex account or an eligible SuperGrok / X Premium subscription without pasting an API key. ### Telegram bot token diff --git a/package.json b/package.json index ee7c6e1..1559bee 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,6 @@ }, "dependencies": { "express": "^5.1.0", - "http-proxy": "^1.18.1", - "node-pty": "^1.0.0" - }, - "pnpm": { - "onlyBuiltDependencies": ["node-pty"] + "http-proxy": "^1.18.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2406e98..3b7417e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: http-proxy: specifier: ^1.18.1 version: 1.18.1 - node-pty: - specifier: ^1.0.0 - version: 1.1.0 packages: @@ -199,12 +196,6 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - - node-pty@1.1.0: - resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -490,12 +481,6 @@ snapshots: negotiator@1.0.0: {} - node-addon-api@7.1.1: {} - - node-pty@1.1.0: - dependencies: - node-addon-api: 7.1.1 - object-inspect@1.13.4: {} on-finished@2.4.1: diff --git a/src/public/setup.html b/src/public/setup.html index 9a8bef9..7d3c2df 100644 --- a/src/public/setup.html +++ b/src/public/setup.html @@ -29,9 +29,6 @@ slackAppToken: '', model: '', log: '', - loginUrl: '', - loginCode: '', - loginLabel: 'Device Login', setupError: '', setupStreamTail: '', loading: false, @@ -71,15 +68,17 @@ ].includes(this.selectedAuth); }, + get isDeviceCode() { + return [ + 'openai-codex', + 'openai-codex-device-code', + 'xai-device-code' + ].includes(this.selectedAuth); + }, + get authGuidance() { - if (this.selectedAuth === 'openai-codex-device-code') { - return 'Run setup, then open the ChatGPT/Codex URL and enter the device code shown in the log.'; - } - if (this.selectedAuth === 'xai-device-code') { - return 'Run setup, then open the xAI URL and enter the device code shown in the log. Use an eligible SuperGrok or X Premium account.'; - } - if (this.selectedAuth === 'openai-codex') { - return 'Browser login needs an interactive terminal to paste the redirect URL. Device pairing is recommended here.'; + if (this.isDeviceCode) { + return 'This login uses an interactive device-code flow that the web setup can\'t run. Open the Railway console and run `openclaw wizard` to complete it, then return here.'; } if (!this.authRequiresSecret) { return 'No API key is needed for this auth method.'; @@ -137,9 +136,10 @@ }, async runSetup() { - this.loginUrl = ''; - this.loginCode = ''; - this.loginLabel = this.loginLabelForAuth(this.selectedAuth); + if (this.isDeviceCode) { + this.setupError = 'This login must be completed from the Railway console with `openclaw wizard`.'; + return; + } this.setupError = ''; this.setupStreamTail = ''; const payload = { @@ -200,28 +200,9 @@ this.log += chunk; const scanText = this.setupStreamTail + chunk; this.setupStreamTail = scanText.slice(-1000); - this.extractLoginDetails(scanText); this.extractSetupFailure(scanText); }, - extractLoginDetails(text) { - const urlMatch = text.match(/https:\/\/auth\.openai\.com\/codex\/device[^\s)]*/) - || text.match(/\bURL:\s*(https:\/\/(?:auth|accounts)\.x\.ai\/[^\s)]*)/) - || text.match(/\bOpen(?: manually)?:\s*(https:\/\/(?:auth|accounts)\.x\.ai\/[^\s)]*)/); - if (urlMatch) this.loginUrl = urlMatch[0]; - if (urlMatch && urlMatch[1]) this.loginUrl = urlMatch[1]; - - const codeMatches = [...text.matchAll(/\bCode:\s*([A-Z0-9][A-Z0-9-]{3,})\b/g)]; - const visibleCode = codeMatches.map(m => m[1]).find(code => code !== 'shown'); - if (visibleCode) this.loginCode = visibleCode; - }, - - loginLabelForAuth(auth) { - if (auth === 'xai-device-code') return 'xAI Login'; - if (auth === 'openai-codex-device-code') return 'ChatGPT Login'; - return 'Device Login'; - }, - extractSetupFailure(text) { if (text.includes('[setup] Internal error')) { this.setupError = 'Setup hit an internal error. Review the log below.'; @@ -856,8 +837,14 @@ openclaw models set anthropic/claude-sonnet-4-20250514 +
+ This login uses an interactive device-code flow that the web setup can't run. + Open the Railway console and run openclaw wizard to + complete it, then return here. Pick a different auth method to continue in the wizard. +
+
-
-
-
- - -
- -
-
diff --git a/src/public/styles.css b/src/public/styles.css index f11c354..748407b 100644 --- a/src/public/styles.css +++ b/src/public/styles.css @@ -629,42 +629,6 @@ img { max-width: 100%; height: auto; } overflow-y: auto; } -.login-code-panel { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - background: rgba(14, 165, 233, 0.08); - border: 1px solid rgba(14, 165, 233, 0.24); - border-radius: 0.5rem; - padding: 1rem; - margin-top: 1rem; - margin-bottom: 1rem; -} - -.login-code-label { - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - color: var(--text-secondary); - margin-bottom: 0.25rem; -} - -.login-code-url { - color: #0284c7; - font-size: 0.875rem; - word-break: break-all; -} - -.login-code-value { - font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; - font-size: 1.5rem; - font-weight: 700; - letter-spacing: 0.08em; - color: #0369a1; - white-space: nowrap; -} - .setup-error-panel { background: rgba(220, 38, 38, 0.08); border: 1px solid rgba(220, 38, 38, 0.24); @@ -677,12 +641,6 @@ img { max-width: 100%; height: auto; } } @media (prefers-color-scheme: dark) { - .login-code-panel { - background: rgba(14, 165, 233, 0.12); - border-color: rgba(56, 189, 248, 0.32); - } - .login-code-url { color: #38bdf8; } - .login-code-value { color: #7dd3fc; } .setup-error-panel { background: rgba(220, 38, 38, 0.12); border-color: rgba(248, 113, 113, 0.32); diff --git a/src/server.js b/src/server.js index cc37cc1..7c5c174 100644 --- a/src/server.js +++ b/src/server.js @@ -8,7 +8,6 @@ import { pathToFileURL } from "node:url"; import express from "express"; import httpProxy from "http-proxy"; -import pty from "node-pty"; import { canServeGatewayRequest, describeGatewayHealth, @@ -131,20 +130,6 @@ function stripAnsi(value) { .replace(/\x1b\[[\x20-\x3f]*[\x40-\x7e]/g, ""); } -function isTransientProgressLine(line) { - return /^[\s◐◓◑◒⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏.-]*(Requesting device code|Waiting for device authorization|Exchanging device code)/.test( - line, - ); -} - -function cleanPtyOutput(value) { - const cleaned = stripAnsi(value) - .split(/\r|\n/) - .filter((line) => line && !isTransientProgressLine(line)) - .join("\n"); - return cleaned ? `${cleaned}\n` : ""; -} - let deviceBootstrapSdkPromise = null; function resolveDeviceBootstrapSdkPath() { @@ -868,20 +853,7 @@ app.get("/setup/api/status", requireSetupAuth, async (_req, res) => { }); }); -function requiresInteractiveOnboarding(payload) { - return [ - "openai-codex-device-code", - "xai-device-code", - ].includes(payload.authChoice); -} - -function interactiveOnboardingLabel(payload) { - if (payload.authChoice === "xai-device-code") return "xAI device code"; - return "OpenAI Codex device pairing"; -} - function buildOnboardArgs(payload) { - const interactive = requiresInteractiveOnboarding(payload); const args = [ "onboard", "--accept-risk", @@ -899,21 +871,10 @@ function buildOnboardArgs(payload) { OPENCLAW_GATEWAY_TOKEN, "--flow", "quickstart", + "--non-interactive", + "--json", ]; - if (interactive) { - args.push( - "--mode", - "local", - "--skip-channels", - "--skip-skills", - "--skip-search", - "--skip-ui", - ); - } else { - args.push("--non-interactive", "--json"); - } - if (payload.authChoice) { args.push("--auth-choice", payload.authChoice); @@ -1010,59 +971,6 @@ function runCmd(cmd, args, opts = {}) { }); } -function runPtyCmd(cmd, args, opts = {}) { - return new Promise((resolve) => { - let out = ""; - const autoInputs = opts.autoInputs ?? []; - const sentAutoInputs = new Set(); - let proc; - try { - proc = pty.spawn(cmd, args, { - name: "xterm-color", - cols: 100, - rows: 30, - cwd: opts.cwd ?? process.cwd(), - env: { - ...process.env, - OPENCLAW_STATE_DIR: STATE_DIR, - OPENCLAW_WORKSPACE_DIR: WORKSPACE_DIR, - // Force OpenClaw's local device-code branch so Railway setup can show - // the short code in the web UI instead of hiding it as remote-only. - DISPLAY: process.env.DISPLAY || ":0", - WAYLAND_DISPLAY: process.env.WAYLAND_DISPLAY || "wayland-0", - SSH_CLIENT: "", - SSH_TTY: "", - SSH_CONNECTION: "", - FORCE_COLOR: "0", - NO_COLOR: "1", - }, - }); - } catch (err) { - out += `\n[spawn error] ${String(err)}\n`; - opts.onOutput?.(out); - resolve({ code: 127, output: out }); - return; - } - - proc.onData((data) => { - const chunk = opts.cleanOutput ? cleanPtyOutput(data) : stripAnsi(data); - if (!chunk) return; - out += chunk; - for (const { input, pattern } of autoInputs) { - const key = String(pattern); - if (sentAutoInputs.has(key) || !pattern.test(out)) continue; - sentAutoInputs.add(key); - proc.write(input); - } - opts.onOutput?.(chunk); - }); - - proc.onExit(({ exitCode }) => { - resolve({ code: exitCode ?? 0, output: out }); - }); - }); -} - const VALID_AUTH_CHOICES = [ "apiKey", "openai-api-key", @@ -1116,12 +1024,21 @@ const VALID_AUTH_CHOICES = [ "custom-api-key", ]; +// Auth methods that rely on an interactive device-code / browser login. These +// can't be driven from the web wizard — direct the operator to run +// `openclaw wizard` from the Railway console instead. +const RAILWAY_CONSOLE_AUTH_CHOICES = [ + "openai-codex", + "openai-codex-device-code", + "xai-device-code", +]; + function validatePayload(payload) { if (payload.authChoice && !VALID_AUTH_CHOICES.includes(payload.authChoice)) { return `Invalid authChoice: ${payload.authChoice}`; } - if (payload.authChoice === "openai-codex") { - return "OpenAI Codex browser login needs redirect-url input in an interactive terminal. Choose OpenAI Codex device pairing in web setup."; + if (RAILWAY_CONSOLE_AUTH_CHOICES.includes(payload.authChoice)) { + return "This login uses an interactive device-code flow that the web setup can't run. Open the Railway console and run `openclaw wizard` to complete it, then return here."; } const stringFields = [ "telegramToken", @@ -1172,19 +1089,11 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => { }); const onboardArgs = buildOnboardArgs(payload); - const interactive = requiresInteractiveOnboarding(payload); - stream( - interactive - ? `Starting ${interactiveOnboardingLabel(payload)}. Use the URL and code below, then keep this page open until it completes.\n\n` - : "Starting OpenClaw onboarding...\n\n", - ); + stream("Starting OpenClaw onboarding...\n\n"); - const onboardRunner = interactive ? runPtyCmd : runCmd; - const onboard = await onboardRunner(OPENCLAW_NODE, clawArgs(onboardArgs), { + const onboard = await runCmd(OPENCLAW_NODE, clawArgs(onboardArgs), { onOutput: stream, - cleanOutput: interactive, - stripOutput: !interactive, - autoInputs: interactive ? [{ pattern: /Enable hooks\?/, input: " \r" }] : [], + stripOutput: true, }); const ok = onboard.code === 0 && isConfigured();