mirror of
https://github.com/arjunkomath/openclaw-railway-template.git
synced 2026-08-14 00:48:11 +00:00
Remove interactive model setup
This commit is contained in:
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-15
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user