Remove interactive model setup

This commit is contained in:
Arjun Komath
2026-06-06 19:50:19 +10:00
parent 2881c71c92
commit d273978e71
6 changed files with 45 additions and 220 deletions
+7 -9
View File
@@ -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
+1 -5
View File
@@ -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"
}
}
-15
View File
@@ -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:
+21 -42
View File
@@ -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</code></pre>
</dl>
</div>
<div x-show="isDeviceCode" class="setup-error-panel">
This login uses an interactive device-code flow that the web setup can't run.
Open the Railway console and run <code class="code-inline">openclaw wizard</code> to
complete it, then return here. Pick a different auth method to continue in the wizard.
</div>
<div class="btn-row">
<button @click="runSetup()" :disabled="loading" class="btn btn-primary">
<button @click="runSetup()" :disabled="loading || isDeviceCode" class="btn btn-primary">
<span x-show="!loading">Run Setup</span>
<span x-show="loading" style="display:inline-flex;align-items:center;gap:0.5rem">
<svg class="spinner-inline" fill="none" viewBox="0 0 24 24"><circle style="opacity:0.25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path style="opacity:0.75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
@@ -869,14 +856,6 @@ openclaw models set anthropic/claude-sonnet-4-20250514</code></pre>
</button>
</div>
<div x-show="loginUrl || loginCode" class="login-code-panel">
<div>
<p class="login-code-label" x-text="loginLabel"></p>
<a x-show="loginUrl" :href="loginUrl" target="_blank" rel="noopener noreferrer" class="login-code-url" x-text="loginUrl"></a>
</div>
<div x-show="loginCode" class="login-code-value" x-text="loginCode"></div>
</div>
<div x-show="setupError" class="setup-error-panel" x-text="setupError"></div>
<div x-show="log" x-ref="setupLog" class="log-output" x-text="log"></div>
-42
View File
@@ -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);
+16 -107
View File
@@ -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();