Remove TUI, encourage railway console

This commit is contained in:
Arjun Komath
2026-06-06 19:36:58 +10:00
parent 0fb97064dc
commit 2881c71c92
8 changed files with 92 additions and 573 deletions
+1 -42
View File
@@ -10,7 +10,6 @@
- **OpenClaw Gateway + Control UI** (served at `/` and `/openclaw`)
- A friendly **Setup Wizard** at `/setup` (protected by a password)
- Optional **Web Terminal** at `/tui` for browser-based TUI access
- Persistent state via **Railway Volume** (so config/credentials/memory survive redeploys)
## How it works (high level)
@@ -49,36 +48,6 @@ This uses OpenClaw's `xai-device-code` onboarding flow, so you can use an eligib
4. Copy the **Bot Token** and paste it into `/setup`
5. Invite the bot to your server (OAuth2 URL Generator → scopes: `bot`, `applications.commands`; then choose permissions)
## Web Terminal (TUI)
The template includes an optional web-based terminal that runs `openclaw tui` in your browser.
### Enabling
Set `ENABLE_WEB_TUI=true` in your Railway Variables. The terminal is **disabled by default**.
Once enabled, access it at `/tui` or via the "Open Terminal" button on the setup page.
### Security
The web TUI implements multiple security layers:
| Control | Description |
|---------|-------------|
| **Opt-in only** | Disabled by default, requires explicit `ENABLE_WEB_TUI=true` |
| **Password protected** | Uses the same `SETUP_PASSWORD` as the setup wizard |
| **Single session** | Only 1 concurrent TUI session allowed at a time |
| **Idle timeout** | Auto-closes after 5 minutes of inactivity (configurable via `TUI_IDLE_TIMEOUT_MS`) |
| **Max duration** | Hard limit of 30 minutes per session (configurable via `TUI_MAX_SESSION_MS`) |
### Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_WEB_TUI` | `false` | Set to `true` to enable |
| `TUI_IDLE_TIMEOUT_MS` | `300000` (5 min) | Closes session after inactivity |
| `TUI_MAX_SESSION_MS` | `1800000` (30 min) | Maximum session duration |
## Local testing
```bash
@@ -87,14 +56,12 @@ docker build -t openclaw-railway-template .
docker run --rm -p 8080:8080 \
-e PORT=8080 \
-e SETUP_PASSWORD=test \
-e ENABLE_WEB_TUI=true \
-e OPENCLAW_STATE_DIR=/data/.openclaw \
-e OPENCLAW_WORKSPACE_DIR=/data/workspace \
-v $(pwd)/.tmpdata:/data \
openclaw-railway-template
# Setup wizard: http://localhost:8080/setup (password: test)
# Web terminal: http://localhost:8080/tui (after setup)
```
## FAQ
@@ -107,10 +74,6 @@ A: Go to `/setup` on your deployed instance. When prompted for credentials, use
A: Go back to `/setup` and click the "Open OpenClaw UI" button from there. The setup page passes the required auth token to the UI. Accessing the UI directly without the token will cause connection errors.
**Q: I don't see the TUI option on the setup page.**
A: Make sure `ENABLE_WEB_TUI=true` is set in your Railway Variables and redeploy. The web terminal is disabled by default.
**Q: How do I approve pairing for Telegram or Discord?**
A: Go to `/setup` and use the "Approve Pairing" dialog to approve pending pairing requests from your chat channels.
@@ -121,7 +84,7 @@ A: New browsers/devices need a one-time approval from the gateway. Go to `/setup
**Q: How do I change the AI model after setup?**
A: Use the OpenClaw CLI to switch models. Access the web terminal at `/tui` (if enabled) or SSH into your container and run:
A: Use the OpenClaw CLI to switch models. Railway provides an integrated in-browser shell directly in your dashboard — no external SSH client needed. Open the Railway dashboard, click your service, go to the **Console** tab, and use the connected shell to run:
```bash
openclaw models set provider/model-id
@@ -143,10 +106,6 @@ A: Go to `/setup` and click the "Run Doctor" button. This runs `openclaw doctor
<img width="2110" height="2032" alt="CleanShot 2026-02-23 at 21 57 59@2x" src="https://github.com/user-attachments/assets/28640eec-fa35-42f2-ba56-cb1fbb9525de" />
## TUI
<img width="2510" height="608" alt="CleanShot 2026-02-23 at 22 08 20@2x" src="https://github.com/user-attachments/assets/61147ec2-ddd5-4b5b-b9ac-0dd81a1ae4c7" />
## Device approval
<img width="1712" height="1376" alt="CleanShot 2026-02-23 at 21 59 21@2x" src="https://github.com/user-attachments/assets/f30ab683-dbc2-4980-ace7-152265e00c79" />
-1
View File
@@ -8,6 +8,5 @@ services:
- SETUP_PASSWORD=test
- OPENCLAW_STATE_DIR=/data/.openclaw
- OPENCLAW_WORKSPACE_DIR=/data/workspace
- ENABLE_WEB_TUI=true
volumes:
- ./.tmpdata:/data
+1 -2
View File
@@ -14,8 +14,7 @@
"dependencies": {
"express": "^5.1.0",
"http-proxy": "^1.18.1",
"node-pty": "^1.0.0",
"ws": "^8.18.0"
"node-pty": "^1.0.0"
},
"pnpm": {
"onlyBuiltDependencies": ["node-pty"]
-17
View File
@@ -17,9 +17,6 @@ importers:
node-pty:
specifier: ^1.0.0
version: 1.1.0
ws:
specifier: ^8.18.0
version: 8.19.0
packages:
@@ -302,18 +299,6 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
snapshots:
accepts@2.0.0:
@@ -627,5 +612,3 @@ snapshots:
vary@1.1.2: {}
wrappy@1.0.2: {}
ws@8.19.0: {}
+20 -6
View File
@@ -42,7 +42,6 @@
pairingMessage: '',
showPairingModal: false,
isReady: false,
tuiEnabled: false,
exporting: false,
showExportInfo: false,
importing: false,
@@ -108,7 +107,6 @@
const j = await this.httpJson('/setup/api/status');
this.configured = j.configured;
this.openclawVersion = j.openclawVersion || '';
this.tuiEnabled = j.tuiEnabled || false;
this.status = j.configured ? 'Configured - open /openclaw' : 'Not configured - run setup below';
this.authGroups = j.authGroups || [];
if (this.authGroups.length > 0) {
@@ -533,10 +531,6 @@
Open OpenClaw UI
<svg class="icon-link" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
</a>
<a x-show="tuiEnabled" href="/tui" target="_blank" class="inline-link link-purple">
Open Terminal
<svg class="icon-link" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
</a>
</div>
</div>
@@ -680,6 +674,26 @@
</div>
<div x-show="isReady && !configured" x-cloak>
<div class="console-card mb-6">
<div class="console-card-header">
<div class="console-icon">
<svg class="icon-md" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
</div>
<h3 class="card-title" style="font-size:1rem;margin-bottom:0">Prefer the command line?</h3>
</div>
<p class="console-card-desc">
This wizard is just one way to get started. You can also run any <code>openclaw</code> command directly using
Railway's built-in <strong>Console</strong> — an in-browser shell scoped to this service, no SSH client needed.
</p>
<ol class="console-steps">
<li>Open your project in the <strong>Railway dashboard</strong></li>
<li>Click this <strong>service</strong>, then open the <strong>Console</strong> tab</li>
<li>Use the connected shell (top) and file browser (bottom) to run commands, e.g.:</li>
</ol>
<pre class="console-cmd"><code>openclaw onboard
openclaw models set anthropic/claude-sonnet-4-20250514</code></pre>
</div>
<div class="steps">
<template x-for="s in totalSteps" :key="s">
<div class="step-item">
+70 -120
View File
@@ -102,6 +102,76 @@ img { max-width: 100%; height: auto; }
.card-compact { padding: 1.25rem; }
.console-card {
background: var(--bg-card);
border: 1px solid #818cf8;
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: var(--shadow-card);
}
.console-card-header {
display: flex;
align-items: center;
gap: 0.625rem;
margin-bottom: 0.75rem;
}
.console-icon {
width: 2rem;
height: 2rem;
border-radius: 0.5rem;
background: rgba(99,102,241,0.15);
color: #6366f1;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.console-card-desc {
font-size: 0.875rem;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 0.875rem;
}
.console-card-desc code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.8125rem;
color: #6366f1;
}
.console-steps {
font-size: 0.875rem;
color: var(--text-secondary);
line-height: 1.6;
margin: 0 0 0.875rem;
padding-left: 1.25rem;
}
.console-cmd {
background: var(--bg-muted);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 0.75rem 0.875rem;
margin: 0;
overflow-x: auto;
}
.console-cmd code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.8125rem;
color: var(--text);
white-space: pre;
}
@media (prefers-color-scheme: dark) {
.console-card { border-color: #312e81; }
.console-icon { color: #818cf8; }
.console-card-desc code { color: #818cf8; }
}
.card + .card,
.card-group > * + * { margin-top: 1.5rem; }
@@ -230,14 +300,10 @@ img { max-width: 100%; height: auto; }
.link-teal { color: #0d9488; transition: color 0.15s; }
.link-teal:hover { color: #14b8a6; }
.link-purple { color: #9333ea; transition: color 0.15s; }
.link-purple:hover { color: #a855f7; }
@media (prefers-color-scheme: dark) {
.link-teal { color: #2dd4bf; }
.link-teal:hover { color: #5eead4; }
.link-purple { color: #c084fc; }
.link-purple:hover { color: #d8b4fe; }
}
.inline-link {
@@ -1081,125 +1147,9 @@ img { max-width: 100%; height: auto; }
.fade-slide-start { opacity: 0; transform: translateX(1rem); }
.fade-slide-end { opacity: 1; transform: translateX(0); }
.tui-body {
background: #0a0a0a;
color: #e5e5e5;
overflow: hidden;
}
.tui-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid #262626;
}
.tui-logo-section {
display: flex;
align-items: center;
gap: 0.75rem;
}
.tui-logo { height: 1.5rem; }
.tui-label {
color: #737373;
font-size: 0.875rem;
}
.tui-status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.status-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
}
.status-dot-yellow { background: #eab308; }
.status-dot-green { background: #22c55e; }
.status-dot-red { background: #ef4444; }
.status-text { color: #a3a3a3; }
.tui-loading-overlay {
position: absolute;
inset: 0;
top: 56px;
background: #0a0a0a;
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
}
.tui-loading-text {
color: #a3a3a3;
font-size: 0.875rem;
}
.tui-spinner {
width: 2rem;
height: 2rem;
border: 2px solid #404040;
border-top-color: #a855f7;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 1rem;
}
.tui-error-overlay {
position: fixed;
inset: 0;
background: rgba(10,10,10,0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.tui-error-card {
background: #171717;
border: 1px solid #262626;
border-radius: 0.5rem;
padding: 1.5rem;
max-width: 28rem;
margin: 0 1rem;
text-align: center;
}
.tui-error-title {
color: #f87171;
font-size: 1.125rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.tui-error-msg {
color: #a3a3a3;
font-size: 0.875rem;
margin-bottom: 1rem;
}
.tui-reconnect-btn {
background: #dc2626;
color: #fff;
border: none;
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-family: var(--font-sans);
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}
.tui-reconnect-btn:hover { background: #b91c1c; }
.log-autoscroll-toggle {
display: inline-flex;
align-items: center;
-193
View File
@@ -1,193 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🦞 OpenClaw TUI</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
<link rel="stylesheet" href="/styles.css">
</head>
<body class="tui-body">
<div class="tui-header">
<div class="tui-logo-section">
<picture>
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.png" alt="OpenClaw" class="tui-logo">
</picture>
<span class="tui-label">Terminal</span>
</div>
<div id="status" class="tui-status">
<span class="status-dot status-dot-yellow"></span>
<span class="status-text">Connecting...</span>
</div>
</div>
<div id="loading-overlay" class="tui-loading-overlay">
<div class="text-center">
<div class="tui-spinner"></div>
<p class="tui-loading-text">Starting terminal session...</p>
</div>
</div>
<div id="terminal-container"></div>
<div id="error-overlay" class="tui-error-overlay hidden">
<div class="tui-error-card">
<div class="tui-error-title" id="error-title">Connection Error</div>
<div class="tui-error-msg" id="error-message"></div>
<button onclick="location.reload()" class="tui-reconnect-btn">
Reconnect
</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
<script>
const statusEl = document.getElementById('status');
const errorOverlay = document.getElementById('error-overlay');
const errorTitle = document.getElementById('error-title');
const errorMessage = document.getElementById('error-message');
const loadingOverlay = document.getElementById('loading-overlay');
let sessionStarted = false;
function setStatus(state, text) {
const colors = {
connecting: 'status-dot-yellow',
connected: 'status-dot-green',
disconnected: 'status-dot-red',
error: 'status-dot-red'
};
statusEl.innerHTML = `
<span class="status-dot ${colors[state] || colors.error}"></span>
<span class="status-text">${text}</span>
`;
}
function showError(title, message) {
loadingOverlay.style.display = 'none';
errorTitle.textContent = title;
errorMessage.textContent = message;
errorOverlay.classList.remove('hidden');
}
const term = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
scrollback: 0,
scrollOnUserInput: false,
smoothScrollDuration: 0,
allowTransparency: false,
theme: {
background: '#0a0a0a',
foreground: '#e5e5e5',
cursor: '#e5e5e5',
cursorAccent: '#0a0a0a',
selectionBackground: '#404040',
black: '#0a0a0a',
red: '#ef4444',
green: '#22c55e',
yellow: '#eab308',
blue: '#3b82f6',
magenta: '#a855f7',
cyan: '#06b6d4',
white: '#e5e5e5',
brightBlack: '#525252',
brightRed: '#f87171',
brightGreen: '#4ade80',
brightYellow: '#facc15',
brightBlue: '#60a5fa',
brightMagenta: '#c084fc',
brightCyan: '#22d3ee',
brightWhite: '#fafafa'
}
});
const fitAddon = new FitAddon.FitAddon();
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
term.loadAddon(fitAddon);
term.loadAddon(webLinksAddon);
const container = document.getElementById('terminal-container');
term.open(container);
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${location.host}/tui/ws`;
let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 3;
function connect() {
setStatus('connecting', 'Connecting...');
sessionStarted = false;
loadingOverlay.style.display = 'flex';
ws = new WebSocket(wsUrl);
ws.onopen = () => {
setStatus('connected', 'Connected');
reconnectAttempts = 0;
fitAddon.fit();
ws.send(JSON.stringify({
type: 'resize',
cols: term.cols,
rows: term.rows
}));
};
ws.onmessage = (event) => {
if (!sessionStarted) {
sessionStarted = true;
loadingOverlay.style.display = 'none';
}
term.write(event.data);
};
ws.onerror = () => {
setStatus('error', 'Connection error');
};
ws.onclose = (event) => {
setStatus('disconnected', 'Disconnected');
if (event.code === 4001) {
showError('Session Limit Reached', 'Another TUI session is already active. Only one session is allowed at a time.');
} else if (event.code === 4002) {
showError('Session Expired', 'Your session has been closed due to inactivity or timeout.');
} else if (event.code === 4003) {
showError('TUI Disabled', 'The web TUI is not enabled on this instance. Set ENABLE_WEB_TUI=true to enable it.');
} else if (event.code === 1008) {
showError('Authentication Required', 'Please authenticate to access the TUI.');
} else if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
setTimeout(connect, 1000 * reconnectAttempts);
} else {
showError('Connection Lost', 'Unable to reconnect to the terminal. Please refresh the page.');
}
};
}
term.onData((data) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'input', data }));
}
});
term.onResize(({ cols, rows }) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
}
});
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => fitAddon.fit(), 200);
});
connect();
</script>
</body>
</html>
-192
View File
@@ -9,7 +9,6 @@ import { pathToFileURL } from "node:url";
import express from "express";
import httpProxy from "http-proxy";
import pty from "node-pty";
import { WebSocketServer } from "ws";
import {
canServeGatewayRequest,
describeGatewayHealth,
@@ -122,16 +121,6 @@ const OPENCLAW_ENTRY =
process.env.OPENCLAW_ENTRY?.trim() || "/openclaw/dist/entry.js";
const OPENCLAW_NODE = process.env.OPENCLAW_NODE?.trim() || "node";
const ENABLE_WEB_TUI = process.env.ENABLE_WEB_TUI?.toLowerCase() === "true";
const TUI_IDLE_TIMEOUT_MS = Number.parseInt(
process.env.TUI_IDLE_TIMEOUT_MS ?? "300000",
10,
);
const TUI_MAX_SESSION_MS = Number.parseInt(
process.env.TUI_MAX_SESSION_MS ?? "1800000",
10,
);
function clawArgs(args) {
return [OPENCLAW_ENTRY, ...args];
}
@@ -876,7 +865,6 @@ app.get("/setup/api/status", requireSetupAuth, async (_req, res) => {
gatewayTarget: GATEWAY_TARGET,
openclawVersion: version,
authGroups,
tuiEnabled: ENABLE_WEB_TUI,
});
});
@@ -1820,148 +1808,6 @@ app.get("/setup/api/logs/stream", requireSetupAuth, (req, res) => {
});
});
app.get("/tui", requireSetupAuth, (_req, res) => {
if (!ENABLE_WEB_TUI) {
return res
.status(403)
.type("text/plain")
.send("Web TUI is disabled. Set ENABLE_WEB_TUI=true to enable it.");
}
if (!isConfigured()) {
return res.redirect("/setup");
}
res.sendFile(path.join(process.cwd(), "src", "public", "tui.html"));
});
let activeTuiSession = null;
function verifyTuiAuth(req) {
if (!SETUP_PASSWORD) return false;
const header = req.headers.authorization || "";
const [scheme, encoded] = header.split(" ");
if (scheme !== "Basic" || !encoded) return false;
const decoded = Buffer.from(encoded, "base64").toString("utf8");
const idx = decoded.indexOf(":");
const password = idx >= 0 ? decoded.slice(idx + 1) : "";
const passwordHash = crypto.createHash("sha256").update(password).digest();
const expectedHash = crypto.createHash("sha256").update(SETUP_PASSWORD).digest();
return crypto.timingSafeEqual(passwordHash, expectedHash);
}
function createTuiWebSocketServer(httpServer) {
const wss = new WebSocketServer({ noServer: true });
wss.on("connection", (ws, req) => {
const clientIp = req.socket?.remoteAddress || "unknown";
log.info("tui", `session started from ${clientIp}`);
let ptyProcess = null;
let idleTimer = null;
let maxSessionTimer = null;
activeTuiSession = {
ws,
pty: null,
startedAt: Date.now(),
lastActivity: Date.now(),
};
function resetIdleTimer() {
if (activeTuiSession) {
activeTuiSession.lastActivity = Date.now();
}
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
log.info("tui", "session idle timeout");
ws.close(4002, "Idle timeout");
}, TUI_IDLE_TIMEOUT_MS);
}
function spawnPty(cols, rows) {
if (ptyProcess) return;
log.info("tui", `spawning PTY with ${cols}x${rows}`);
ptyProcess = pty.spawn(OPENCLAW_NODE, clawArgs(["tui"]), {
name: "xterm-256color",
cols,
rows,
cwd: WORKSPACE_DIR,
env: {
...process.env,
OPENCLAW_STATE_DIR: STATE_DIR,
OPENCLAW_WORKSPACE_DIR: WORKSPACE_DIR,
TERM: "xterm-256color",
},
});
if (activeTuiSession) {
activeTuiSession.pty = ptyProcess;
}
idleTimer = setTimeout(() => {
log.info("tui", "session idle timeout");
ws.close(4002, "Idle timeout");
}, TUI_IDLE_TIMEOUT_MS);
maxSessionTimer = setTimeout(() => {
log.info("tui", "max session duration reached");
ws.close(4002, "Max session duration");
}, TUI_MAX_SESSION_MS);
ptyProcess.onData((data) => {
if (ws.readyState === ws.OPEN) {
ws.send(data);
}
});
ptyProcess.onExit(({ exitCode, signal }) => {
log.info("tui", `PTY exited code=${exitCode} signal=${signal}`);
if (ws.readyState === ws.OPEN) {
ws.close(1000, "Process exited");
}
});
}
ws.on("message", (message) => {
resetIdleTimer();
try {
const msg = JSON.parse(message.toString());
if (msg.type === "resize" && msg.cols && msg.rows) {
const cols = Math.min(Math.max(msg.cols, 10), 500);
const rows = Math.min(Math.max(msg.rows, 5), 200);
if (!ptyProcess) {
spawnPty(cols, rows);
} else {
ptyProcess.resize(cols, rows);
}
} else if (msg.type === "input" && msg.data && ptyProcess) {
ptyProcess.write(msg.data);
}
} catch (err) {
log.warn("tui", `invalid message: ${err.message}`);
}
});
ws.on("close", () => {
log.info("tui", "session closed");
clearTimeout(idleTimer);
clearTimeout(maxSessionTimer);
if (ptyProcess) {
try {
ptyProcess.kill();
} catch {}
}
activeTuiSession = null;
});
ws.on("error", (err) => {
log.error("tui", `WebSocket error: ${err.message}`);
});
});
return wss;
}
const proxy = httpProxy.createProxyServer({
target: GATEWAY_TARGET,
ws: true,
@@ -2047,7 +1893,6 @@ app.use(async (req, res) => {
const server = app.listen(PORT, () => {
log.info("wrapper", `listening on port ${PORT}`);
log.info("wrapper", `setup wizard: http://localhost:${PORT}/setup`);
log.info("wrapper", `web TUI: ${ENABLE_WEB_TUI ? "enabled" : "disabled"}`);
log.info("wrapper", `configured: ${isConfigured()}`);
void probeDeviceBootstrapSdk();
@@ -2068,36 +1913,7 @@ const server = app.listen(PORT, () => {
}
});
const tuiWss = createTuiWebSocketServer(server);
server.on("upgrade", async (req, socket, head) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === "/tui/ws") {
if (!ENABLE_WEB_TUI) {
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
socket.destroy();
return;
}
if (!verifyTuiAuth(req)) {
socket.write("HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"OpenClaw TUI\"\r\n\r\n");
socket.destroy();
return;
}
if (activeTuiSession) {
socket.write("HTTP/1.1 409 Conflict\r\n\r\n");
socket.destroy();
return;
}
tuiWss.handleUpgrade(req, socket, head, (ws) => {
tuiWss.emit("connection", ws, req);
});
return;
}
if (!isConfigured()) {
socket.destroy();
return;
@@ -2120,14 +1936,6 @@ async function gracefulShutdown(signal) {
clearInterval(setupRateLimiter.cleanupInterval);
}
if (activeTuiSession) {
try {
activeTuiSession.ws.close(1001, "Server shutting down");
activeTuiSession.pty.kill();
} catch {}
activeTuiSession = null;
}
server.close();
if (gatewayProc) {