fix: a token-gated or LAN-bound dashboard works end to end (WSL mirrored)
- /ws-pty carries the dashboard token like /ws (the terminal never attached with DASHBOARD_TOKEN set), and so does everything that cannot send the header: images, downloads, exports, drops (withToken/tokenHeader). A refused upgrade answers 401/403 and is logged and counted by reason. - The browser asks for a missing or wrong token (401 EUNAUTHORIZED) and takes ?token= from the link agenthub start prints, dropping it from the URL. - The CLI sends the token (DASHBOARD_TOKEN, else the one the running server publishes 0600 in <data dir>/runtime/dashboard-token); MCP and statusline read the same file. - DASHBOARD_HOST takes a list, and 127.0.0.1 is always listened on too, so a LAN-only bind keeps hooks and the CLI working. - agenthub start --host/--allowed-hosts/--token are saved in <data dir>/network.json for every later start; a wide bind gets a token; every reachable URL is printed. - tmux < 3.7 is an amber, once-per-version notice that says only the pane scrollbar is missing, with a ~/.local build instead of sudo make install. - INSTALL: PATH belongs in ~/.profile for sudo -iu/ssh; WSL mirrored notes.
This commit is contained in:
+28
-5
@@ -99,7 +99,9 @@ const { writeServerInfo, removeServerInfo, peersSharingDataDir } = require("./li
|
||||
const { getDataDir } = require("./lib/claude-home");
|
||||
const {
|
||||
resolveHost,
|
||||
isLoopbackHostname,
|
||||
extraListenHosts,
|
||||
isExposedBind,
|
||||
accessUrls,
|
||||
corsOptions,
|
||||
hostGuard,
|
||||
tokenGuard,
|
||||
@@ -158,6 +160,8 @@ const APP_VERSION = (() => {
|
||||
})();
|
||||
|
||||
function createApp() {
|
||||
// Saved network settings first: the CORS options below are read once here.
|
||||
require("./lib/network-config").applyNetworkConfig();
|
||||
const app = express();
|
||||
// Express 5 parses a query string with node's `querystring` by default;
|
||||
// keep Express 4's `qs` parsing, which every route and guard (`namedIds`
|
||||
@@ -293,6 +297,9 @@ function createApp() {
|
||||
}
|
||||
|
||||
function startServer(app, port) {
|
||||
// `agenthub start --host/--allowed-hosts/--token`, saved for every start;
|
||||
// a variable set in the environment wins.
|
||||
require("./lib/network-config").applyNetworkConfig();
|
||||
const server = http.createServer(app);
|
||||
initWebSocket(server);
|
||||
initPtyWebSocket(server);
|
||||
@@ -368,8 +375,12 @@ function startServer(app, port) {
|
||||
// Bind to loopback by default so the dashboard is not network-reachable out
|
||||
// of the box (GHSA-gr74-4xfh-6jw9). Operators opt into a wider bind with
|
||||
// DASHBOARD_HOST=0.0.0.0 — and are warned to set DASHBOARD_TOKEN when they do.
|
||||
const host = resolveHost();
|
||||
const boundLoopback = isLoopbackHostname(host);
|
||||
// DASHBOARD_HOST may list several addresses; the first is `server`'s own
|
||||
// bind, every other one (127.0.0.1 included whenever no address covers it,
|
||||
// so hooks and the CLI keep working) a listener handing its connections to
|
||||
// `server` — the same app, WebSockets and peer checks, the real socket kept.
|
||||
const host = resolveHost().replace(/^\[(.*)\]$/, "$1");
|
||||
const boundLoopback = !isExposedBind();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// A port another program holds fails here, not as an uncaught 'error'
|
||||
@@ -377,10 +388,22 @@ function startServer(app, port) {
|
||||
server.once("error", reject);
|
||||
server.listen(port, host, () => {
|
||||
server.off("error", reject);
|
||||
const boundPort = server.address().port;
|
||||
const extras = extraListenHosts().map((h) => {
|
||||
const l = require("node:net").createServer((sock) => server.emit("connection", sock));
|
||||
l.on("error", (e) =>
|
||||
console.warn(`⚠️ Could not also listen on ${h}:${boundPort} — ${e.code || e.message}`)
|
||||
);
|
||||
l.listen(boundPort, h);
|
||||
return l;
|
||||
});
|
||||
if (extras.length) server.on("close", () => extras.forEach((l) => l.close()));
|
||||
// Publish the live port so the Claude Code hook handler can find this
|
||||
// server even when it bound a non-default port (the desktop app falls
|
||||
// back off 4820 when that port is already taken).
|
||||
writeServerInfo(port);
|
||||
// The token (if any) for a local CLI/MCP/statusline that never exported it.
|
||||
require("./lib/security").publishDashboardToken();
|
||||
// The AI Proxy gateway URL a server-mode pane is given names this port.
|
||||
require("./lib/proxy-gateway").setListenPort(server.address().port);
|
||||
// S3: what members' panes run — this install's scripts and every
|
||||
@@ -413,8 +436,8 @@ function startServer(app, port) {
|
||||
);
|
||||
}
|
||||
const mode = isProduction ? "production" : "development";
|
||||
const shown = boundLoopback ? "localhost" : host;
|
||||
console.log(`Agent Dashboard server running on http://${shown}:${port} (${mode})`);
|
||||
const urls = accessUrls(boundPort);
|
||||
console.log(`Agent Dashboard server running on ${urls.join(" ")} (${mode})`);
|
||||
if (isServerMode()) {
|
||||
console.log("Server mode: every page and API call needs a login.");
|
||||
if (!hasAdmin()) {
|
||||
|
||||
Reference in New Issue
Block a user