From f453d61dd3485b4b4fef032bd1311ced5896a0da Mon Sep 17 00:00:00 2001
From: Balamurugan Marimuthu <246387390+bmarimuthu-nv@users.noreply.github.com>
Date: Fri, 1 May 2026 19:34:37 -0700
Subject: [PATCH 1/3] Clarify README onboarding flows
Signed-off-by: Balamurugan Marimuthu <246387390+bmarimuthu-nv@users.noreply.github.com>
---
README.md | 84 ++++++++++++++++++++++++++-----------------------------
1 file changed, 40 insertions(+), 44 deletions(-)
diff --git a/README.md b/README.md
index ff45c95..9c5c4c2 100644
--- a/README.md
+++ b/README.md
@@ -4,17 +4,17 @@
maglev
-Run AI coding shells locally, then control them from your browser or phone.
+Run AI coding sessions locally, then control them from your browser or phone.
## Start Here
-Pick the setup that matches where your shell will run:
+Pick the setup that matches where your sessions will run:
| Use case | Best fit | Open the UI from |
|----------|----------|------------------|
-| Local laptop or devbox | Hub and shell on the same machine | `http://localhost:3006` |
-| SSH workstation | Hub on the remote machine, browser through an SSH tunnel | `http://localhost:3006` on your laptop |
-| Slurm/HPC node | Hub inside the allocation, broker on a reachable login/VNC node | The URL printed by `maglev hub start --remote` |
+| Local laptop or devbox | Hub and sessions on the same machine | The URL printed by `maglev hub start` |
+| SSH workstation | Hub on the remote machine, browser through an SSH tunnel | The forwarded URL on your laptop |
+| Slurm/HPC node | Server on a reachable login/VNC node, hub inside the allocation | The URL printed by `maglev hub start --remote` |
## Install
@@ -78,25 +78,28 @@ bun run build:standalone
## Local Setup
-Use this when your browser and coding shell are on the same machine.
+Use this when your browser and coding environment are on the same machine.
```bash
maglev hub start --name local
-maglev shell
```
Open:
```text
-http://localhost:3006
+The URL printed by `maglev hub start`.
```
-Optional: start the runner if you want the web UI to create new shells later.
+By default, `maglev hub start` chooses a free local port to avoid conflicts. If you want the traditional fixed local URL, pin the port explicitly:
```bash
-maglev runner start
+maglev hub start --name local --port 3006
```
+Then open `http://localhost:3006`.
+
+Create sessions from the web UI. `maglev hub start` also starts the local runner for that hub, so you do not need to run `maglev shell` or `maglev runner start` manually for the normal flow.
+
## SSH Setup
Use this when Maglev runs on a remote workstation but you browse from your laptop.
@@ -105,69 +108,62 @@ On the remote workstation:
```bash
maglev hub start --name devbox --host 127.0.0.1
-maglev shell
```
+Use the port printed by `maglev hub start` in your SSH tunnel. Example, if the hub prints `http://127.0.0.1:43891`:
+
On your laptop:
```bash
-ssh -L 3006:127.0.0.1:3006 user@devbox
+ssh -L 43891:127.0.0.1:43891 user@devbox
```
Then open this on your laptop:
```text
-http://localhost:3006
+http://localhost:43891
```
-Optional: keep remote spawning available from the web UI:
+If you prefer a stable tunnel command, start the remote hub with `--port 3006` and forward `3006`.
-```bash
-maglev runner start
-```
+Create sessions from the web UI after the runner appears in the machine list.
## Slurm / HPC Setup
-Use this when shells run on ephemeral compute nodes that your browser cannot reach directly.
+Use this when sessions run on ephemeral compute nodes that your browser cannot reach directly.
-On a stable login, VNC, or jump node:
+Assumption: the login node and allocated Slurm node/container share the same home directory. At minimum, they must share `~/.maglev`. The server writes connection and auth state there, and `maglev hub start --remote` reads it inside the allocation. If your site gives jobs a different home directory, mount or bind the login node's `~/.maglev` into the job/container before starting the hub.
-```bash
-# Terminal 1: keep the broker running
-maglev server
+| Where | Run | Purpose |
+|-------|-----|---------|
+| Client laptop/browser | Open the URL printed by `maglev hub start --remote` | Use the web UI |
+| Login, VNC, or jump node | `maglev server service install` | Keep the Maglev server reachable |
+| Login, VNC, or jump node | `maglev auth github login` | Authenticate browser access once |
+| Slurm node/container | `maglev hub start --name "slurm-${SLURM_JOB_ID:-manual}" --remote` | Start the hub and runner inside the allocation |
-# Terminal 2: authenticate browser access once
+The default Linux setup is to run the server as a user service on the stable login/VNC/jump node:
+
+```bash
+maglev server service install
maglev auth github login
```
-Inside the Slurm allocation:
+Then, inside the Slurm allocation:
```bash
srun --pty bash
maglev hub start --name "slurm-${SLURM_JOB_ID:-manual}" --remote
-maglev shell
```
Open the URL printed by `maglev hub start --remote`.
-If the browser cannot reach the broker hostname, start the broker with the public URL you actually use:
+If the browser cannot reach the server hostname, start the server with the public URL you actually use:
```bash
-maglev server --public-url https://your-reachable-broker.example
+maglev server --public-url https://your-reachable-server.example
```
-If the login node and compute node do not share `~/.maglev`, pass broker details explicitly:
-
-```bash
-# On the login/VNC node
-cat ~/.maglev/broker-url
-cat ~/.maglev/broker-key
-
-# Inside the Slurm allocation
-maglev hub start --remote \
- --broker-url http://login-node:3010 \
- --broker-token ""
-```
+If Linux user services are not available on the login/VNC/jump node, keep `maglev server` running in a terminal or under your site's preferred process manager.
## Daily Commands
@@ -185,11 +181,11 @@ maglev server hubs
## Services
-For long-running Linux user services:
+For long-running Linux hosts, install services instead of keeping foreground terminals open:
```bash
-maglev hub service install
maglev server service install
+maglev hub service install
```
For named hubs:
@@ -203,9 +199,9 @@ maglev hub logs --name devbox-a --follow
## Mental Model
- `maglev hub` stores session state and serves the web UI.
-- `maglev shell` starts a shell session and registers it with the hub.
-- `maglev runner` lets the web UI spawn new shells on that machine.
-- `maglev server` is the optional broker for machines your browser cannot reach directly.
+- `maglev hub start` starts the matching runner automatically.
+- `maglev runner` lets the web UI create sessions on that machine; direct runner commands are mostly for status, logs, and troubleshooting.
+- `maglev server` is the remote access entrypoint for machines your browser cannot reach directly.
## More Docs
From bebc0d57d8f879db74ab1a0f37236d1d91957075 Mon Sep 17 00:00:00 2001
From: Balamurugan Marimuthu <246387390+bmarimuthu-nv@users.noreply.github.com>
Date: Fri, 1 May 2026 19:49:49 -0700
Subject: [PATCH 2/3] Rename remote access broker URLs to server URLs
Signed-off-by: Balamurugan Marimuthu <246387390+bmarimuthu-nv@users.noreply.github.com>
---
README.md | 38 ++++++++++++--
cli/src/commands/hub.test.ts | 16 +++---
cli/src/commands/hub.ts | 58 +++++++++++----------
cli/src/commands/server.ts | 40 +++++++-------
cli/src/runtime/assets.ts | 2 +-
docs/guide/faq.md | 4 +-
docs/guide/how-it-works.md | 4 +-
docs/guide/installation.md | 12 +++--
docs/guide/why-maglev.md | 24 ++++-----
docs/public/schemas/settings.schema.json | 5 ++
hub/src/broker/client.ts | 22 ++++----
hub/src/broker/index.ts | 66 +++++++++++++-----------
hub/src/broker/key.ts | 38 ++++++++++++--
hub/src/config/serverSettings.ts | 35 ++++++++-----
hub/src/config/settings.ts | 3 +-
hub/src/configuration.ts | 10 ++--
hub/src/index.ts | 50 +++++++++---------
hub/src/web/brokerSession.ts | 2 +-
hub/src/web/routes/auth.ts | 8 +--
hub/src/web/server.ts | 2 +-
20 files changed, 263 insertions(+), 176 deletions(-)
diff --git a/README.md b/README.md
index 9c5c4c2..779b4da 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,24 @@ Pick the setup that matches where your sessions will run:
| SSH workstation | Hub on the remote machine, browser through an SSH tunnel | The forwarded URL on your laptop |
| Slurm/HPC node | Server on a reachable login/VNC node, hub inside the allocation | The URL printed by `maglev hub start --remote` |
+## Direct vs Server Mode
+
+Maglev does not guess whether your browser can reach a hub. You choose the mode when starting the hub.
+
+Use direct mode when your browser can reach the hub URL directly, including through an SSH tunnel:
+
+```bash
+maglev hub start --name devbox
+```
+
+Use server mode when the hub runs somewhere your browser cannot reach directly, such as inside a Slurm allocation or container:
+
+```bash
+maglev hub start --name "slurm-${SLURM_JOB_ID:-manual}" --remote
+```
+
+In server mode, the hub still listens on a local port, but it also registers with `maglev server`. The server URL is discovered from `~/.maglev/server-url` written by `maglev server`, from saved settings, or from `--server-url `.
+
## Install
Fast path: install the latest prebuilt release for your machine.
@@ -78,7 +96,7 @@ bun run build:standalone
## Local Setup
-Use this when your browser and coding environment are on the same machine.
+Use this direct-mode setup when your browser and coding environment are on the same machine.
```bash
maglev hub start --name local
@@ -102,7 +120,7 @@ Create sessions from the web UI. `maglev hub start` also starts the local runner
## SSH Setup
-Use this when Maglev runs on a remote workstation but you browse from your laptop.
+Use this direct-mode setup when Maglev runs on a remote workstation and your browser reaches it through an SSH tunnel.
On the remote workstation:
@@ -110,7 +128,7 @@ On the remote workstation:
maglev hub start --name devbox --host 127.0.0.1
```
-Use the port printed by `maglev hub start` in your SSH tunnel. Example, if the hub prints `http://127.0.0.1:43891`:
+By default, the remote hub also chooses a free local port to avoid conflicts. Use the port printed by `maglev hub start` in your SSH tunnel. Example, if the hub prints `http://127.0.0.1:43891`:
On your laptop:
@@ -124,13 +142,23 @@ Then open this on your laptop:
http://localhost:43891
```
-If you prefer a stable tunnel command, start the remote hub with `--port 3006` and forward `3006`.
+If you prefer a stable tunnel command, pin the remote hub port:
+
+```bash
+maglev hub start --name devbox --host 127.0.0.1 --port 3006
+```
+
+Then forward `3006`:
+
+```bash
+ssh -L 3006:127.0.0.1:3006 user@devbox
+```
Create sessions from the web UI after the runner appears in the machine list.
## Slurm / HPC Setup
-Use this when sessions run on ephemeral compute nodes that your browser cannot reach directly.
+Use this server-mode setup when sessions run on ephemeral compute nodes that your browser cannot reach directly.
Assumption: the login node and allocated Slurm node/container share the same home directory. At minimum, they must share `~/.maglev`. The server writes connection and auth state there, and `maglev hub start --remote` reads it inside the allocation. If your site gives jobs a different home directory, mount or bind the login node's `~/.maglev` into the job/container before starting the hub.
diff --git a/cli/src/commands/hub.test.ts b/cli/src/commands/hub.test.ts
index 4b80199..cec043e 100644
--- a/cli/src/commands/hub.test.ts
+++ b/cli/src/commands/hub.test.ts
@@ -17,7 +17,7 @@ describe('hub command arg filtering', () => {
'--name', 'cw-devbox-1',
'--remote',
'--config', '/tmp/maglev-config-Sbn3W8.yaml',
- '--broker-url', 'http://broker:3010',
+ '--server-url', 'http://server:3010',
'--host', '127.0.0.1',
'--port', '15115'
])
@@ -25,12 +25,12 @@ describe('hub command arg filtering', () => {
expect(args).toEqual([
'--remote',
'--config', '/tmp/maglev-config-Sbn3W8.yaml',
- '--broker-url', 'http://broker:3010',
+ '--server-url', 'http://server:3010',
'--host', '127.0.0.1',
'--port', '15115'
])
expect(__test__.parseHubArgs(args)).toEqual({
- brokerUrl: 'http://broker:3010',
+ serverUrl: 'http://server:3010',
configPath: '/tmp/maglev-config-Sbn3W8.yaml',
host: '127.0.0.1',
port: '15115'
@@ -55,21 +55,21 @@ describe('hub command arg filtering', () => {
[
'--remote',
'--config', '/tmp/old.yaml',
- '--broker-url', 'http://old-broker:3010',
+ '--server-url', 'http://old-server:3010',
'--host', '127.0.0.1',
'--port', '15115'
],
[
'--remote',
'--config', '/tmp/new.yaml',
- '--broker-url', 'http://new-broker:3010'
+ '--server-url', 'http://new-server:3010'
]
)
expect(args).toEqual([
'--remote',
'--config', '/tmp/new.yaml',
- '--broker-url', 'http://new-broker:3010',
+ '--server-url', 'http://new-server:3010',
'--host', '127.0.0.1',
'--port', '15115'
])
@@ -80,7 +80,7 @@ describe('hub command arg filtering', () => {
[
'--remote',
'--config', '/tmp/current.yaml',
- '--broker-url', 'http://broker:3010',
+ '--server-url', 'http://server:3010',
'--port', '15115'
],
['--remote']
@@ -89,7 +89,7 @@ describe('hub command arg filtering', () => {
expect(args).toEqual([
'--remote',
'--config', '/tmp/current.yaml',
- '--broker-url', 'http://broker:3010',
+ '--server-url', 'http://server:3010',
'--port', '15115'
])
})
diff --git a/cli/src/commands/hub.ts b/cli/src/commands/hub.ts
index 1664233..546ed6d 100644
--- a/cli/src/commands/hub.ts
+++ b/cli/src/commands/hub.ts
@@ -13,7 +13,7 @@ import type { CommandDefinition, CommandContext } from './types'
const HUB_SERVICE_NAME = 'maglev-hub.service'
const HUB_DAEMON_DIR = join(configuration.maglevHomeDir, 'hub-daemons')
-type ParsedHubArgs = { host?: string; port?: string; brokerUrl?: string; brokerToken?: string; name?: string; configPath?: string; debug?: boolean }
+type ParsedHubArgs = { host?: string; port?: string; serverUrl?: string; serverToken?: string; name?: string; configPath?: string; debug?: boolean }
type HubDaemonState = {
name: string
pid: number
@@ -73,10 +73,10 @@ function parseHubArgs(args: string[]): ParsedHubArgs {
result.host = args[++i]
} else if (arg === '--port' && i + 1 < args.length) {
result.port = args[++i]
- } else if (arg === '--broker-url' && i + 1 < args.length) {
- result.brokerUrl = args[++i]
- } else if (arg === '--broker-token' && i + 1 < args.length) {
- result.brokerToken = args[++i]
+ } else if ((arg === '--server-url' || arg === '--broker-url') && i + 1 < args.length) {
+ result.serverUrl = args[++i]
+ } else if ((arg === '--server-token' || arg === '--broker-token') && i + 1 < args.length) {
+ result.serverToken = args[++i]
} else if (arg === '--name' && i + 1 < args.length) {
result.name = args[++i]
} else if (arg === '--config' && i + 1 < args.length) {
@@ -87,10 +87,14 @@ function parseHubArgs(args: string[]): ParsedHubArgs {
result.host = arg.slice('--host='.length)
} else if (arg.startsWith('--port=')) {
result.port = arg.slice('--port='.length)
+ } else if (arg.startsWith('--server-url=')) {
+ result.serverUrl = arg.slice('--server-url='.length)
} else if (arg.startsWith('--broker-url=')) {
- result.brokerUrl = arg.slice('--broker-url='.length)
+ result.serverUrl = arg.slice('--broker-url='.length)
+ } else if (arg.startsWith('--server-token=')) {
+ result.serverToken = arg.slice('--server-token='.length)
} else if (arg.startsWith('--broker-token=')) {
- result.brokerToken = arg.slice('--broker-token='.length)
+ result.serverToken = arg.slice('--broker-token='.length)
} else if (arg.startsWith('--name=')) {
result.name = arg.slice('--name='.length)
} else if (arg.startsWith('--config=')) {
@@ -115,8 +119,8 @@ ${chalk.bold('Usage:')}
${chalk.bold('Options:')}
--host Bind host
--port Listen port
- --broker-url Broker URL for remote mode
- --broker-token Optional broker registration token
+ --server-url Server URL for remote mode
+ --server-token Optional server registration token
--name Stable hub name (default: hostname)
--config Optional hub config YAML
--debug Run hub in the foreground for debugging
@@ -311,7 +315,7 @@ function ensureMachineIdForHub(name: string): string {
function buildHubRuntimeEnv(args: string[], machineIdOverride?: string): NodeJS.ProcessEnv {
const env = { ...process.env }
- const { host, port, brokerUrl, brokerToken, name, configPath } = parseHubArgs(args)
+ const { host, port, serverUrl, serverToken, name, configPath } = parseHubArgs(args)
if (host) {
env.MAGLEV_LISTEN_HOST = host
@@ -321,11 +325,11 @@ function buildHubRuntimeEnv(args: string[], machineIdOverride?: string): NodeJS.
env.MAGLEV_LISTEN_PORT = port
env.WEBAPP_PORT = port
}
- if (brokerUrl) {
- env.MAGLEV_BROKER_URL = brokerUrl
+ if (serverUrl) {
+ env.MAGLEV_SERVER_URL = serverUrl
}
- if (brokerToken) {
- env.MAGLEV_BROKER_TOKEN = brokerToken
+ if (serverToken) {
+ env.MAGLEV_SERVER_TOKEN = serverToken
}
if (configPath) {
env.MAGLEV_HUB_CONFIG = configPath
@@ -546,7 +550,7 @@ async function stopRunnerForHub(commandArgs: string[], logFd?: number): Promise<
}
function filterDaemonStartArgs(args: string[]): string[] {
- const flagsWithValue = new Set(['--host', '--port', '--broker-url', '--broker-token', '--name', '--config'])
+ const flagsWithValue = new Set(['--host', '--port', '--server-url', '--server-token', '--broker-url', '--broker-token', '--name', '--config'])
const filtered: string[] = []
let skippedPositionalName = false
for (let i = 0; i < args.length; i++) {
@@ -585,7 +589,7 @@ function mergeDaemonArgs(storedArgs: string[] | undefined, overrideArgs: string[
const merged: string[] = []
const overrideMap = new Map()
const passthroughFlags = new Set()
- const valueFlags = new Set(['--host', '--port', '--broker-url', '--broker-token', '--config'])
+ const valueFlags = new Set(['--host', '--port', '--server-url', '--server-token', '--broker-url', '--broker-token', '--config'])
for (let i = 0; i < overrideArgs.length; i++) {
const arg = overrideArgs[i]
@@ -1076,7 +1080,7 @@ export const hubCommand: CommandDefinition = {
if (context.commandArgs[0] === 'daemon-run') {
const passthroughArgs = context.commandArgs.slice(1)
- const { host, port, brokerUrl, brokerToken, name: parsedName, configPath } = parseHubArgs(passthroughArgs)
+ const { host, port, serverUrl, serverToken, name: parsedName, configPath } = parseHubArgs(passthroughArgs)
const name = parsedName || sanitizeDaemonName(hostname() || 'local')
if (host) {
process.env.MAGLEV_LISTEN_HOST = host
@@ -1086,11 +1090,11 @@ export const hubCommand: CommandDefinition = {
process.env.MAGLEV_LISTEN_PORT = port
process.env.WEBAPP_PORT = port
}
- if (brokerUrl) {
- process.env.MAGLEV_BROKER_URL = brokerUrl
+ if (serverUrl) {
+ process.env.MAGLEV_SERVER_URL = serverUrl
}
- if (brokerToken) {
- process.env.MAGLEV_BROKER_TOKEN = brokerToken
+ if (serverToken) {
+ process.env.MAGLEV_SERVER_TOKEN = serverToken
}
if (configPath) {
process.env.MAGLEV_HUB_CONFIG = configPath
@@ -1118,9 +1122,9 @@ export const hubCommand: CommandDefinition = {
return
}
- const { host, port, brokerUrl, brokerToken, name: parsedName, configPath, debug } = parseHubArgs(context.commandArgs)
+ const { host, port, serverUrl, serverToken, name: parsedName, configPath, debug } = parseHubArgs(context.commandArgs)
const name = parsedName || sanitizeDaemonName(hostname() || 'local')
- const allowedPrefixes = ['--host', '--port', '--broker-url', '--broker-token', '--name', '--config', '--remote', '--debug']
+ const allowedPrefixes = ['--host', '--port', '--server-url', '--server-token', '--broker-url', '--broker-token', '--name', '--config', '--remote', '--debug']
const unexpectedArgs = context.commandArgs.filter((arg) => {
if (!arg.startsWith('-')) {
return true
@@ -1145,11 +1149,11 @@ export const hubCommand: CommandDefinition = {
process.env.MAGLEV_LISTEN_PORT = resolved.port
process.env.WEBAPP_PORT = resolved.port
}
- if (resolved.brokerUrl) {
- process.env.MAGLEV_BROKER_URL = resolved.brokerUrl
+ if (resolved.serverUrl) {
+ process.env.MAGLEV_SERVER_URL = resolved.serverUrl
}
- if (resolved.brokerToken) {
- process.env.MAGLEV_BROKER_TOKEN = resolved.brokerToken
+ if (resolved.serverToken) {
+ process.env.MAGLEV_SERVER_TOKEN = resolved.serverToken
}
if (resolved.configPath) {
process.env.MAGLEV_HUB_CONFIG = resolved.configPath
diff --git a/cli/src/commands/server.ts b/cli/src/commands/server.ts
index 5df8947..028ff58 100644
--- a/cli/src/commands/server.ts
+++ b/cli/src/commands/server.ts
@@ -30,7 +30,7 @@ function parseBrokerArgs(args: string[]): ParsedBrokerArgs {
result.port = args[++i]
} else if (arg === '--public-url' && i + 1 < args.length) {
result.publicUrl = args[++i]
- } else if (arg === '--broker-token' && i + 1 < args.length) {
+ } else if ((arg === '--server-token' || arg === '--broker-token') && i + 1 < args.length) {
result.token = args[++i]
} else if (arg.startsWith('--host=')) {
result.host = arg.slice('--host='.length)
@@ -38,6 +38,8 @@ function parseBrokerArgs(args: string[]): ParsedBrokerArgs {
result.port = arg.slice('--port='.length)
} else if (arg.startsWith('--public-url=')) {
result.publicUrl = arg.slice('--public-url='.length)
+ } else if (arg.startsWith('--server-token=')) {
+ result.token = arg.slice('--server-token='.length)
} else if (arg.startsWith('--broker-token=')) {
result.token = arg.slice('--broker-token='.length)
} else {
@@ -244,10 +246,10 @@ function runBrokerServiceCommand(commandArgs: string[]): void {
return
default:
console.log(`
-${chalk.bold('maglev server service')} - Manage the user-level broker daemon
+${chalk.bold('maglev server service')} - Manage the user-level remote access server
${chalk.bold('Usage:')}
- maglev server service install [broker args]
+ maglev server service install [server args]
maglev server service start
maglev server service stop
maglev server service restart
@@ -287,13 +289,13 @@ type BrokerHubsResponse = {
async function listBrokerHubs(): Promise {
const brokerUrlState = await readBrokerUrl()
if (!brokerUrlState?.url) {
- throw new Error('Broker URL not found. Start `maglev server` first so it can write ~/.maglev/broker-url.')
+ throw new Error('Server URL not found. Start `maglev server` first so it can write ~/.maglev/server-url.')
}
const githubAuthState = await readRemoteGitHubAuthState()
const githubAuth = githubAuthState?.state.githubAuth
if (!githubAuth) {
- throw new Error('Broker auth requires cached GitHub auth. Run `maglev auth github login` first.')
+ throw new Error('Server auth requires cached GitHub auth. Run `maglev auth github login` first.')
}
const brokerSessionToken = await signBrokerSessionToken({
@@ -310,13 +312,13 @@ async function listBrokerHubs(): Promise {
const body = await response.json().catch(() => null) as BrokerHubsResponse | null
if (!response.ok) {
- throw new Error(body?.error || `Broker request failed with status ${response.status}`)
+ throw new Error(body?.error || `Server request failed with status ${response.status}`)
}
const activeHubs = body?.hubs ?? []
const recentHubs = body?.recentHubs ?? []
- console.log(chalk.bold(`\nBroker Hubs (${brokerUrlState.url})\n`))
+ console.log(chalk.bold(`\nServer Hubs (${brokerUrlState.url})\n`))
const printFolders = (hub: BrokerHubRecord) => {
if (hub.configError) {
@@ -368,7 +370,7 @@ async function listBrokerHubs(): Promise {
export const serverCommand: CommandDefinition = {
name: 'server',
- description: 'Manage the broker server (start, stop, status)',
+ description: 'Manage the remote access server (start, stop, status)',
requiresRuntimeAssets: false,
run: async (context: CommandContext) => {
try {
@@ -387,7 +389,7 @@ export const serverCommand: CommandDefinition = {
console.log(`
${chalk.bold('maglev server')}
-Start the self-hosted remote broker on a stable machine such as a VNC/login node.
+Start the self-hosted remote access server on a stable machine such as a VNC/login node.
${chalk.bold('Usage:')}
maglev server [options]
@@ -400,19 +402,19 @@ ${chalk.bold('Options:')}
--host Bind host (default: 0.0.0.0)
--port Optional listen port; defaults to a free auto-picked port
--public-url Optional public base URL; defaults to http://:
- --broker-token Optional override for hub registration; default: ~/.maglev/broker-key
+ --server-token Optional override for hub registration; default: ~/.maglev/server-key
${chalk.bold('Environment:')}
- MAGLEV_BROKER_LISTEN_HOST
- MAGLEV_BROKER_LISTEN_PORT
- MAGLEV_BROKER_PUBLIC_URL
- MAGLEV_BROKER_TOKEN
+ MAGLEV_SERVER_LISTEN_HOST
+ MAGLEV_SERVER_LISTEN_PORT
+ MAGLEV_SERVER_PUBLIC_URL
+ MAGLEV_SERVER_TOKEN
${chalk.bold('Example:')}
maglev server
maglev server hubs
maglev server --port 3010
- maglev server --public-url https://vnc-broker.internal
+ maglev server --public-url https://vnc-server.internal
`)
process.exit(0)
}
@@ -422,16 +424,16 @@ ${chalk.bold('Example:')}
}
if (host) {
- process.env.MAGLEV_BROKER_LISTEN_HOST = host
+ process.env.MAGLEV_SERVER_LISTEN_HOST = host
}
if (port) {
- process.env.MAGLEV_BROKER_LISTEN_PORT = port
+ process.env.MAGLEV_SERVER_LISTEN_PORT = port
}
if (publicUrl) {
- process.env.MAGLEV_BROKER_PUBLIC_URL = publicUrl
+ process.env.MAGLEV_SERVER_PUBLIC_URL = publicUrl
}
if (token) {
- process.env.MAGLEV_BROKER_TOKEN = token
+ process.env.MAGLEV_SERVER_TOKEN = token
}
const { startBroker } = await import('../../../hub/src/broker')
diff --git a/cli/src/runtime/assets.ts b/cli/src/runtime/assets.ts
index 35ca585..6f672f0 100644
--- a/cli/src/runtime/assets.ts
+++ b/cli/src/runtime/assets.ts
@@ -1,5 +1,5 @@
export async function ensureRuntimeAssets(): Promise {
// Legacy runtime asset extraction existed only for the retired managed
// tunnel transport. Keep the hook as a no-op so command wiring stays
- // stable while the broker-based model remains the only live path.
+ // stable while the server-based model remains the only live path.
}
diff --git a/docs/guide/faq.md b/docs/guide/faq.md
index d01d1c3..44b915c 100644
--- a/docs/guide/faq.md
+++ b/docs/guide/faq.md
@@ -38,7 +38,7 @@ maglev hub --remote
### What is the access token for?
-`MAGLEV_API_TOKEN` authenticates CLI-to-hub access. In local browser flows it can also be used for manual sign-in. In broker-based remote mode, browser access goes through broker/GitHub auth instead.
+`MAGLEV_API_TOKEN` authenticates CLI-to-hub access. In local browser flows it can also be used for manual sign-in. In server-based remote mode, browser access goes through server/GitHub auth instead.
### Can I use Maglev without Telegram?
@@ -80,7 +80,7 @@ Maglev is local-first:
### Can others access my Maglev instance?
-Only if they can satisfy your configured browser auth path. For public access, use HTTPS and prefer the broker-based remote flow.
+Only if they can satisfy your configured browser auth path. For public access, use HTTPS and prefer the server-based remote flow.
## Troubleshooting
diff --git a/docs/guide/how-it-works.md b/docs/guide/how-it-works.md
index 62a40cb..8b1a286 100644
--- a/docs/guide/how-it-works.md
+++ b/docs/guide/how-it-works.md
@@ -85,10 +85,10 @@ The web app is terminal-first:
Maglev supports:
- local-only use on `localhost`
-- self-hosted remote access through the broker
+- self-hosted remote access through the server
- optional public exposure through your own tunnel or reverse proxy
-In remote mode the broker handles hub registration and browser routing. The session itself still runs on your machine.
+In remote mode the server handles hub registration and browser routing. The session itself still runs on your machine.
## Seamless Handoff
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
index 868c032..688888b 100644
--- a/docs/guide/installation.md
+++ b/docs/guide/installation.md
@@ -99,15 +99,15 @@ maglev auth github login
maglev hub --remote
```
-The broker:
+The server:
- stores state under `~/.maglev/`
-- writes its public URL to `~/.maglev/broker-url`
-- stores the registration key in `~/.maglev/broker-key`
+- writes its public URL to `~/.maglev/server-url`
+- stores the registration key in `~/.maglev/server-key`
The hub:
-- reads `~/.maglev/broker-url` automatically unless overridden
+- reads `~/.maglev/server-url` automatically unless overridden
- prints the browser URL after registration
- uses GitHub device auth for browser sign-in
@@ -158,6 +158,8 @@ The runner keeps a local control server plus a list of tracked shell sessions.
| `MAGLEV_LISTEN_HOST` | `127.0.0.1` | Hub bind address |
| `MAGLEV_LISTEN_PORT` | `3006` | Hub port |
| `MAGLEV_PUBLIC_URL` | - | Public HTTPS URL |
+| `MAGLEV_SERVER_URL` | `~/.maglev/server-url` | Remote access server URL for `maglev hub --remote` |
+| `MAGLEV_SERVER_TOKEN` | `~/.maglev/server-key` | Optional hub registration token override |
| `CORS_ORIGINS` | - | Allowed browser origins |
| `MAGLEV_HOME` | `~/.maglev` | Config directory |
| `DB_PATH` | `~/.maglev/maglev.db` | Database file |
@@ -197,6 +199,6 @@ maglev hub logs --name devbox-a --follow
## Notes
-- `maglev server` runs the remote broker for coordinating hubs across machines
+- `maglev server` runs the remote access server for coordinating hubs across machines
- browser access is terminal-first; sessions are shells, not chat agents
- files and review continue to work for shell sessions
diff --git a/docs/guide/why-maglev.md b/docs/guide/why-maglev.md
index 045c83b..aeca0fb 100644
--- a/docs/guide/why-maglev.md
+++ b/docs/guide/why-maglev.md
@@ -10,9 +10,9 @@ The short version: Happy is built around a centralized service. Maglev is built
|--------|-------|--------|
| **Architecture** | Centralized cloud service | Local-first, self-hosted |
| **Data location** | Stored on the service, encrypted | Stored on your machine |
-| **Remote access** | Through the hosted service | Through your hub directly, or through your self-hosted broker |
+| **Remote access** | Through the hosted service | Through your hub directly, or through your self-hosted server |
| **Deployment** | Multi-service stack | Single workspace / self-hosted components |
-| **Main complexity** | E2EE, key handling, service ops | Running your own hub and optional broker |
+| **Main complexity** | E2EE, key handling, service ops | Running your own hub and optional server |
Choose Maglev if you want local ownership, self-hosting, and a simple mental model.
@@ -40,33 +40,33 @@ For remote access, Maglev supports two practical shapes:
1. Direct self-hosting
- expose the hub yourself with HTTPS, a reverse proxy, Tailscale, Cloudflare Tunnel, or similar
-2. Broker-based remote access
+2. Server-based remote access
- run `maglev server` on a stable machine you control
- run `maglev hub --remote` on the machine that hosts the sessions
- - the hub opens an outbound broker connection
- - the broker routes browser HTTP, SSE, and WebSocket traffic back to that hub
+ - the hub opens an outbound server connection
+ - the server routes browser HTTP, SSE, and WebSocket traffic back to that hub
-The key point is that the broker is also yours. There is no managed Maglev relay service in the architecture.
+The key point is that the server is also yours. There is no managed Maglev relay service in the architecture.
## Maglev Remote Architecture
```text
┌──────────────┐ HTTPS / WS ┌────────────────┐ persistent WS ┌──────────────┐
│ Browser/PWA │ ◄────────────────►│ Self-hosted │◄───────────────────►│ Hub │
-│ or Phone │ │ Broker │ │ + Sessions │
+│ or Phone │ │ Server │ │ + Sessions │
└──────────────┘ └────────────────┘ └──────────────┘
│
▼
local SQLite + files
```
-What the broker does:
+What the server does:
- gives you a stable public URL
- keeps track of live hubs
- forwards browser traffic to the right hub
-What the broker does not do:
+What the server does not do:
- store your session data
- own your long-term application state
@@ -82,9 +82,9 @@ Maglev assumes you control the infrastructure:
- local-only mode: browser talks to your hub directly
- self-hosted public mode: you secure the hub with your own HTTPS/reverse proxy setup
-- broker mode: you secure the browser-to-broker path, and the broker forwards traffic to your hub over the hub's outbound session
+- server mode: you secure the browser-to-server path, and the server forwards traffic to your hub over the hub's outbound session
-For internet-facing broker mode, browser sign-in is handled through GitHub-backed broker auth, then the hub issues its own JWT for app access.
+For internet-facing server mode, browser sign-in is handled through GitHub-backed server auth, then the hub issues its own JWT for app access.
## Why The Architectures Diverge
@@ -100,7 +100,7 @@ That leads to different tradeoffs:
| Dimension | Happy | Maglev |
|-----------|-------|--------|
-| **Server role** | Primary shared backend | Your own hub and optional broker |
+| **Server role** | Primary shared backend | Your own hub and optional server |
| **State storage** | Hosted service | Local hub |
| **Scaling model** | Shared multi-tenant infra | Per-user or per-team self-hosting |
| **Trust boundary** | Hosted backend must not see plaintext | You control the machines and network path |
diff --git a/docs/public/schemas/settings.schema.json b/docs/public/schemas/settings.schema.json
index 30ce312..48f00f1 100644
--- a/docs/public/schemas/settings.schema.json
+++ b/docs/public/schemas/settings.schema.json
@@ -32,6 +32,11 @@
"format": "uri",
"description": "Public URL for external access (e.g., Telegram Mini App). ENV: MAGLEV_PUBLIC_URL"
},
+ "serverUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "Remote access server URL for hub registration. ENV: MAGLEV_SERVER_URL"
+ },
"corsOrigins": {
"type": "array",
"items": {
diff --git a/hub/src/broker/client.ts b/hub/src/broker/client.ts
index cb63c46..96dd031 100644
--- a/hub/src/broker/client.ts
+++ b/hub/src/broker/client.ts
@@ -174,7 +174,7 @@ export class BrokerClient {
async start(): Promise {
if (this.started) {
if (!this.hubUrl) {
- throw new Error('Broker client started without a hub URL')
+ throw new Error('Server client started without a hub URL')
}
return this.hubUrl
}
@@ -229,7 +229,7 @@ export class BrokerClient {
this.socket = socket
socket.send(JSON.stringify(this.buildRegisterMessage()))
this.reconnecting = false
- this.emitStatus(`Connected to broker ${this.config.brokerUrl}`)
+ this.emitStatus(`Connected to server ${this.config.brokerUrl}`)
resolve()
}
@@ -238,7 +238,7 @@ export class BrokerClient {
return
}
settled = true
- reject(new Error(`Failed to connect to broker WebSocket ${this.wsUrl}`))
+ reject(new Error(`Failed to connect to server WebSocket ${this.wsUrl}`))
}
socket.onclose = () => {
@@ -246,7 +246,7 @@ export class BrokerClient {
return
}
settled = true
- reject(new Error(`Broker WebSocket closed during connect ${this.wsUrl}`))
+ reject(new Error(`Server WebSocket closed during connect ${this.wsUrl}`))
}
})
@@ -263,7 +263,7 @@ export class BrokerClient {
if (wasActiveSocket) {
this.socket = null
this.closeProxySockets()
- this.emitStatus(`Disconnected from broker ${this.config.brokerUrl}`)
+ this.emitStatus(`Disconnected from server ${this.config.brokerUrl}`)
this.scheduleReconnect()
}
}
@@ -312,7 +312,7 @@ export class BrokerClient {
}
this.reconnecting = true
- this.emitStatus(`Broker connection lost; retrying in ${BrokerClient.RECONNECT_DELAY_MS / 1000}s`)
+ this.emitStatus(`Server connection lost; retrying in ${BrokerClient.RECONNECT_DELAY_MS / 1000}s`)
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null
void this.reconnect()
@@ -326,12 +326,12 @@ export class BrokerClient {
}
try {
- this.emitStatus(`Reconnecting to broker ${this.config.brokerUrl}`)
+ this.emitStatus(`Reconnecting to server ${this.config.brokerUrl}`)
await this.connect()
- this.emitStatus(`Re-registered hub ${this.hubId} with broker`)
+ this.emitStatus(`Re-registered hub ${this.hubId} with server`)
} catch (error) {
this.emitStatus(
- `Broker reconnect failed: ${error instanceof Error ? error.message : String(error)}`
+ `Server reconnect failed: ${error instanceof Error ? error.message : String(error)}`
)
this.reconnecting = false
this.scheduleReconnect()
@@ -349,7 +349,7 @@ export class BrokerClient {
try {
message = JSON.parse(text) as BrokerMessage
} catch {
- console.error('[Broker] Received malformed JSON message, ignoring')
+ console.error('[Server] Received malformed JSON message, ignoring')
return
}
@@ -365,7 +365,7 @@ export class BrokerClient {
try {
response = await this.forwardProxyRequest(message)
} catch (error) {
- console.error(`[Broker] Failed to proxy ${message.method} ${message.path}:`, error)
+ console.error(`[Server] Failed to proxy ${message.method} ${message.path}:`, error)
response = createProxyErrorResponse(message.requestId, error)
}
if (this.socket?.readyState === WebSocket.OPEN) {
diff --git a/hub/src/broker/index.ts b/hub/src/broker/index.ts
index 38ffd6a..e472286 100644
--- a/hub/src/broker/index.ts
+++ b/hub/src/broker/index.ts
@@ -142,13 +142,17 @@ function getEnv(name: string): string | undefined {
return value ? value : undefined
}
+function getEnvWithLegacy(name: string, legacyName: string): string | undefined {
+ return getEnv(name) ?? getEnv(legacyName)
+}
+
async function findFreePort(): Promise {
return await new Promise((resolve, reject) => {
const server = createServer()
server.listen(0, '0.0.0.0', () => {
const address = server.address()
if (!address || typeof address === 'string') {
- server.close(() => reject(new Error('Failed to determine an available broker port')))
+ server.close(() => reject(new Error('Failed to determine an available server port')))
return
}
const { port } = address
@@ -165,21 +169,21 @@ async function findFreePort(): Promise {
}
async function getBrokerConfig(): Promise {
- const host = getEnv('MAGLEV_BROKER_LISTEN_HOST') ?? '0.0.0.0'
- const portValue = getEnv('MAGLEV_BROKER_LISTEN_PORT')
+ const host = getEnvWithLegacy('MAGLEV_SERVER_LISTEN_HOST', 'MAGLEV_BROKER_LISTEN_HOST') ?? '0.0.0.0'
+ const portValue = getEnvWithLegacy('MAGLEV_SERVER_LISTEN_PORT', 'MAGLEV_BROKER_LISTEN_PORT')
let port: number
if (portValue) {
port = Number.parseInt(portValue, 10)
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
- throw new Error(`Invalid MAGLEV_BROKER_LISTEN_PORT: ${portValue}`)
+ throw new Error(`Invalid MAGLEV_SERVER_LISTEN_PORT: ${portValue}`)
}
} else {
port = await findFreePort()
}
- const publicUrl = getEnv('MAGLEV_BROKER_PUBLIC_URL') ?? `http://${getHostname()}:${port}`
- const configuredToken = getEnv('MAGLEV_BROKER_TOKEN')
+ const publicUrl = getEnvWithLegacy('MAGLEV_SERVER_PUBLIC_URL', 'MAGLEV_BROKER_PUBLIC_URL') ?? `http://${getHostname()}:${port}`
+ const configuredToken = getEnvWithLegacy('MAGLEV_SERVER_TOKEN', 'MAGLEV_BROKER_TOKEN')
if (configuredToken) {
return {
host,
@@ -1015,7 +1019,7 @@ function createTimeoutPromise(requestId: string, targetClientId?: string): Promi
async function proxyHttpRequest(config: BrokerConfig, hub: RegisteredHub, req: Request, tailPath: string, session: BrokerUserSession): Promise {
if (!hub.socket) {
- return new Response('Hub is not connected to the broker', { status: 503 })
+ return new Response('Hub is not connected to the server', { status: 503 })
}
const url = new URL(req.url)
@@ -1087,17 +1091,17 @@ export async function startBroker(): Promise {
const authConfig = await getBrokerAuthConfig()
const brokerUrlPath = await writeBrokerUrl(config.publicUrl)
- console.log('Maglev Broker starting...')
- console.log(`[Broker] Listen host: ${config.host}`)
- console.log(`[Broker] Listen port: ${config.port}`)
- console.log(`[Broker] Public URL: ${config.publicUrl}`)
- console.log(`[Broker] URL file: ${brokerUrlPath}`)
+ console.log('Maglev Server starting...')
+ console.log(`[Server] Listen host: ${config.host}`)
+ console.log(`[Server] Listen port: ${config.port}`)
+ console.log(`[Server] Public URL: ${config.publicUrl}`)
+ console.log(`[Server] URL file: ${brokerUrlPath}`)
if (config.tokenPath) {
- console.log(`[Broker] Registration key: ${config.tokenCreated ? 'created' : 'loaded'} from ${config.tokenPath}`)
+ console.log(`[Server] Registration key: ${config.tokenCreated ? 'created' : 'loaded'} from ${config.tokenPath}`)
} else {
- console.log('[Broker] Registration key: loaded from MAGLEV_BROKER_TOKEN')
+ console.log('[Server] Registration key: loaded from MAGLEV_SERVER_TOKEN')
}
- console.log('[Broker] Mode: self-hosted control plane')
+ console.log('[Server] Mode: self-hosted control plane')
const pruneInterval = setInterval(pruneExpiredHubs, 5_000)
@@ -1113,7 +1117,7 @@ export async function startBroker(): Promise {
if (config.token) {
const providedToken = url.searchParams.get('token')?.trim() || null
if (providedToken !== config.token) {
- return new Response('Broker token mismatch', { status: 401 })
+ return new Response('Server token mismatch', { status: 401 })
}
}
const clientId = randomUUID()
@@ -1136,7 +1140,7 @@ export async function startBroker(): Promise {
const activeHubs = listActiveHubs()
return Response.json({
status: 'ok',
- service: 'maglev-broker',
+ service: 'maglev-server',
requestId: randomUUID(),
activeHubs: activeHubs.length,
recentHubs: listRecentHubs().length
@@ -1145,7 +1149,7 @@ export async function startBroker(): Promise {
if (url.pathname === '/api/github/device/start' && req.method === 'POST') {
if (!authConfig.gitHubDeviceAuth) {
- return Response.json({ error: 'GitHub device auth is not configured for the broker' }, { status: 503 })
+ return Response.json({ error: 'GitHub device auth is not configured for the server' }, { status: 503 })
}
try {
return Response.json(await authConfig.gitHubDeviceAuth.start())
@@ -1156,7 +1160,7 @@ export async function startBroker(): Promise {
if (url.pathname === '/api/github/device/poll' && req.method === 'POST') {
if (!authConfig.gitHubDeviceAuth) {
- return Response.json({ error: 'GitHub device auth is not configured for the broker' }, { status: 503 })
+ return Response.json({ error: 'GitHub device auth is not configured for the server' }, { status: 503 })
}
const body = await req.json().catch(() => null) as { deviceCode?: unknown } | null
const deviceCode = typeof body?.deviceCode === 'string' ? body.deviceCode.trim() : ''
@@ -1198,7 +1202,7 @@ export async function startBroker(): Promise {
if (url.pathname === '/api/hubs' && req.method === 'GET') {
if (!brokerSession) {
- return Response.json({ error: 'Broker session required' }, { status: 401 })
+ return Response.json({ error: 'Server session required' }, { status: 401 })
}
const activeHubs = listActiveHubs()
return Response.json({
@@ -1215,7 +1219,7 @@ export async function startBroker(): Promise {
if (url.pathname.startsWith('/api/hubs/') && req.method === 'GET') {
if (!brokerSession) {
- return Response.json({ error: 'Broker session required' }, { status: 401 })
+ return Response.json({ error: 'Server session required' }, { status: 401 })
}
const hubId = decodeURIComponent(url.pathname.slice('/api/hubs/'.length))
const hub = registry.get(hubId)
@@ -1244,7 +1248,7 @@ export async function startBroker(): Promise {
if (url.pathname.startsWith('/h/')) {
if (!brokerSession) {
- return new Response('Broker session required', { status: 401 })
+ return new Response('Server session required', { status: 401 })
}
const tail = url.pathname.slice('/h/'.length)
const slashIndex = tail.indexOf('/')
@@ -1259,7 +1263,7 @@ export async function startBroker(): Promise {
if (req.headers.get('upgrade')) {
if (!hub.socket) {
- return new Response('Hub is not connected to the broker', { status: 503 })
+ return new Response('Hub is not connected to the server', { status: 503 })
}
const wsId = randomUUID()
const upgraded = serverRef.upgrade(req, {
@@ -1289,7 +1293,7 @@ export async function startBroker(): Promise {
return proxyHttpRequest(config, hub, req, tailPath, brokerSession).catch((error) => {
return new Response(
- error instanceof Error ? error.message : 'Broker proxy error',
+ error instanceof Error ? error.message : 'Server proxy error',
{ status: 502 }
)
})
@@ -1297,7 +1301,7 @@ export async function startBroker(): Promise {
if (url.pathname === '/' || url.pathname === '/index.html') {
if (!brokerSession) {
- return new Response(renderBrokerLogin(config, authConfig.gitHubDeviceAuth ? undefined : 'Broker GitHub auth is not configured.'), {
+ return new Response(renderBrokerLogin(config, authConfig.gitHubDeviceAuth ? undefined : 'Server GitHub auth is not configured.'), {
headers: {
'content-type': 'text/html; charset=utf-8'
}
@@ -1489,19 +1493,19 @@ export async function startBroker(): Promise {
})
console.log('')
- console.log('Maglev Broker is ready!')
- console.log(`[Broker] Local: http://${config.host}:${config.port}`)
- console.log(`[Broker] Public: ${config.publicUrl}`)
+ console.log('Maglev Server is ready!')
+ console.log(`[Server] Local: http://${config.host}:${config.port}`)
+ console.log(`[Server] Public: ${config.publicUrl}`)
const shutdown = () => {
- console.log('\nShutting down broker...')
+ console.log('\nShutting down server...')
clearInterval(pruneInterval)
for (const pending of pendingRequests.values()) {
- pending.reject(new Error('Broker shutting down'))
+ pending.reject(new Error('Server shutting down'))
}
pendingRequests.clear()
for (const controller of pendingStreams.values()) {
- controller.error(new Error('Broker shutting down'))
+ controller.error(new Error('Server shutting down'))
}
pendingStreams.clear()
server.stop()
diff --git a/hub/src/broker/key.ts b/hub/src/broker/key.ts
index ad417c8..9ca0050 100644
--- a/hub/src/broker/key.ts
+++ b/hub/src/broker/key.ts
@@ -9,10 +9,18 @@ function getBrokerHome(): string {
}
export function getBrokerKeyPath(): string {
- return join(getBrokerHome(), 'broker-key')
+ return join(getBrokerHome(), 'server-key')
}
export function getBrokerUrlPath(): string {
+ return join(getBrokerHome(), 'server-url')
+}
+
+function getLegacyBrokerKeyPath(): string {
+ return join(getBrokerHome(), 'broker-key')
+}
+
+function getLegacyBrokerUrlPath(): string {
return join(getBrokerHome(), 'broker-url')
}
@@ -40,11 +48,22 @@ export async function getOrCreateBrokerKey(): Promise<{ key: string; path: strin
if (existsSync(path)) {
const key = (await readFile(path, 'utf8')).trim()
if (!key) {
- throw new Error(`Broker key file is empty: ${path}`)
+ throw new Error(`Server key file is empty: ${path}`)
}
return { key, path, created: false }
}
+ const legacyPath = getLegacyBrokerKeyPath()
+ if (existsSync(legacyPath)) {
+ const key = (await readFile(legacyPath, 'utf8')).trim()
+ if (!key) {
+ throw new Error(`Legacy broker key file is empty: ${legacyPath}`)
+ }
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 })
+ await writeFile(path, `${key}\n`, { mode: 0o600 })
+ return { key, path, created: false }
+ }
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 })
const key = generateBrokerKey()
await writeFile(path, `${key}\n`, { mode: 0o600 })
@@ -61,12 +80,23 @@ export async function writeBrokerUrl(publicUrl: string): Promise {
export async function readBrokerUrl(): Promise<{ url: string; path: string } | null> {
const path = getBrokerUrlPath()
if (!existsSync(path)) {
- return null
+ const legacyPath = getLegacyBrokerUrlPath()
+ if (!existsSync(legacyPath)) {
+ return null
+ }
+
+ const legacyUrl = (await readFile(legacyPath, 'utf8')).trim()
+ if (!legacyUrl) {
+ throw new Error(`Legacy broker URL file is empty: ${legacyPath}`)
+ }
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 })
+ await writeFile(path, `${legacyUrl}\n`, { mode: 0o600 })
+ return { url: legacyUrl, path }
}
const url = (await readFile(path, 'utf8')).trim()
if (!url) {
- throw new Error(`Broker URL file is empty: ${path}`)
+ throw new Error(`Server URL file is empty: ${path}`)
}
return { url, path }
}
diff --git a/hub/src/config/serverSettings.ts b/hub/src/config/serverSettings.ts
index a18eb5d..e9e8164 100644
--- a/hub/src/config/serverSettings.ts
+++ b/hub/src/config/serverSettings.ts
@@ -30,7 +30,7 @@ export interface ServerSettings {
login: string
name?: string
} | null
- brokerUrl: string | null
+ serverUrl: string | null
}
export interface ServerSettingsResult {
@@ -46,7 +46,7 @@ export interface ServerSettingsResult {
githubOwner: 'env' | 'file' | 'default'
githubAllowedUsers: 'env' | 'file' | 'default'
githubAuth: 'file' | 'default'
- brokerUrl: 'env' | 'file' | 'default'
+ serverUrl: 'env' | 'file' | 'default'
}
savedToFile: boolean
}
@@ -125,7 +125,7 @@ export async function loadServerSettings(dataDir: string): Promise file > null
let telegramBotToken: string | null = null
@@ -306,17 +306,28 @@ export async function loadServerSettings(dataDir: string): Promise {
const raw = process.env.MAGLEV_TERMINAL_SUPERVISION_HUMAN_OVERRIDE_MS?.trim()
diff --git a/hub/src/index.ts b/hub/src/index.ts
index 6b092cd..fdba272 100644
--- a/hub/src/index.ts
+++ b/hub/src/index.ts
@@ -106,10 +106,10 @@ async function main() {
// Load configuration (async - loads from env/file with persistence)
const remoteMode = resolveRemoteMode(process.argv)
const config = await createConfiguration()
- const discoveredBrokerUrl = remoteMode && !config.brokerUrl
+ const discoveredServerUrl = remoteMode && !config.serverUrl
? await readBrokerUrl()
: null
- const effectiveBrokerUrl = config.brokerUrl ?? discoveredBrokerUrl?.url ?? null
+ const effectiveServerUrl = config.serverUrl ?? discoveredServerUrl?.url ?? null
const baseCorsOrigins = normalizeOrigins(config.corsOrigins)
const corsOrigins = baseCorsOrigins
@@ -150,11 +150,11 @@ async function main() {
// Display tunnel status
console.log(`[Hub] Remote mode: ${remoteMode ? 'enabled (--remote)' : 'disabled'}`)
if (remoteMode) {
- if (effectiveBrokerUrl) {
- if (config.brokerUrl) {
- console.log(`[Hub] Broker URL: ${config.brokerUrl} (${formatSource(config.sources.brokerUrl)})`)
- } else if (discoveredBrokerUrl) {
- console.log(`[Hub] Broker URL: ${discoveredBrokerUrl.url} (${discoveredBrokerUrl.path})`)
+ if (effectiveServerUrl) {
+ if (config.serverUrl) {
+ console.log(`[Hub] Server URL: ${config.serverUrl} (${formatSource(config.sources.serverUrl)})`)
+ } else if (discoveredServerUrl) {
+ console.log(`[Hub] Server URL: ${discoveredServerUrl.url} (${discoveredServerUrl.path})`)
}
}
} else {
@@ -176,8 +176,8 @@ async function main() {
if (!config.githubOauthClientId) {
throw new Error('Remote mode requires MAGLEV_GITHUB_OAUTH_CLIENT_ID')
}
- if (!effectiveBrokerUrl) {
- throw new Error('Remote mode requires a broker URL. Start `maglev server` first so it can write ~/.maglev/broker-url, or pass `--broker-url`.')
+ if (!effectiveServerUrl) {
+ throw new Error('Remote mode requires a server URL. Start `maglev server` first so it can write ~/.maglev/server-url, or pass `--server-url`.')
}
if (!config.githubOwner && config.githubAllowedUsers.length === 0 && !config.githubAuth) {
throw new Error('Remote mode requires MAGLEV_GITHUB_OWNER, MAGLEV_GITHUB_ALLOWED_USERS, or a bootstrapped owner from `maglev auth github login`')
@@ -288,20 +288,20 @@ async function main() {
throw new Error('Remote mode requires a GitHub owner identity')
}
- const configuredBrokerToken = process.env.MAGLEV_BROKER_TOKEN?.trim() || null
- const brokerKey = configuredBrokerToken
+ const configuredServerToken = process.env.MAGLEV_SERVER_TOKEN?.trim() || process.env.MAGLEV_BROKER_TOKEN?.trim() || null
+ const brokerKey = configuredServerToken
? null
: await getOrCreateBrokerKey()
if (brokerKey) {
- console.log(`[Broker] Registration key: ${brokerKey.created ? 'created' : 'loaded'} from ${brokerKey.path}`)
+ console.log(`[Server] Registration key: ${brokerKey.created ? 'created' : 'loaded'} from ${brokerKey.path}`)
} else {
- console.log('[Broker] Registration key: loaded from MAGLEV_BROKER_TOKEN')
+ console.log('[Server] Registration key: loaded from MAGLEV_SERVER_TOKEN')
}
brokerClient = new BrokerClient({
- brokerUrl: effectiveBrokerUrl!,
- brokerToken: configuredBrokerToken ?? brokerKey?.key ?? null,
+ brokerUrl: effectiveServerUrl!,
+ brokerToken: configuredServerToken ?? brokerKey?.key ?? null,
owner: brokerOwner,
localHost: config.listenHost,
localPort: config.listenPort,
@@ -309,30 +309,30 @@ async function main() {
launchFolders: hubLaunchConfig.folders,
configError: hubLaunchConfig.error,
onStatusChange: (status) => {
- console.log(`[Broker] ${status}`)
+ console.log(`[Server] ${status}`)
}
})
try {
tunnelUrl = await brokerClient.start()
- console.log(`[Broker] Registered hub ${brokerClient.getHubId()}`)
+ console.log(`[Server] Registered hub ${brokerClient.getHubId()}`)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
- const brokerSource = config.brokerUrl
- ? `${config.brokerUrl} (${formatSource(config.sources.brokerUrl)})`
- : discoveredBrokerUrl
- ? `${discoveredBrokerUrl.url} (${discoveredBrokerUrl.path})`
- : effectiveBrokerUrl!
+ const serverSource = config.serverUrl
+ ? `${config.serverUrl} (${formatSource(config.sources.serverUrl)})`
+ : discoveredServerUrl
+ ? `${discoveredServerUrl.url} (${discoveredServerUrl.path})`
+ : effectiveServerUrl!
throw new Error(
- `Remote mode broker registration failed for ${brokerSource}: ${message}. ` +
- 'Pass `--broker-url ` or fix ~/.maglev/broker-url, then retry.'
+ `Remote mode server registration failed for ${serverSource}: ${message}. ` +
+ 'Pass `--server-url ` or fix ~/.maglev/server-url, then retry.'
)
}
}
if (tunnelUrl) {
const announceTunnelAccess = async () => {
- console.log('[Broker] Public: ' + tunnelUrl)
+ console.log('[Server] Public: ' + tunnelUrl)
console.log('')
console.log('Open in browser:')
diff --git a/hub/src/web/brokerSession.ts b/hub/src/web/brokerSession.ts
index e4e123b..7f61683 100644
--- a/hub/src/web/brokerSession.ts
+++ b/hub/src/web/brokerSession.ts
@@ -13,7 +13,7 @@ let brokerSecretPromise: Promise | null = null
async function getBrokerSecret(): Promise {
if (!brokerSecretPromise) {
brokerSecretPromise = (async () => {
- const configured = process.env.MAGLEV_BROKER_TOKEN?.trim()
+ const configured = process.env.MAGLEV_SERVER_TOKEN?.trim() || process.env.MAGLEV_BROKER_TOKEN?.trim()
if (configured) {
return new TextEncoder().encode(configured)
}
diff --git a/hub/src/web/routes/auth.ts b/hub/src/web/routes/auth.ts
index 3e2eef4..f6620af 100644
--- a/hub/src/web/routes/auth.ts
+++ b/hub/src/web/routes/auth.ts
@@ -180,12 +180,12 @@ export function createAuthRoutes(
}
if (!remoteMode) {
- return c.json({ error: 'Broker session login is only available in remote mode' }, 404)
+ return c.json({ error: 'Server session login is only available in remote mode' }, 404)
}
const brokerSession = await getBrokerSessionFromHeaders(c.req.raw.headers)
if (!brokerSession) {
- return c.json({ error: 'Broker session required' }, 401)
+ return c.json({ error: 'Server session required' }, 401)
}
const auth = await signWebJwt(jwtSecret, getCurrentHubNamespace(), {
@@ -204,7 +204,7 @@ export function createAuthRoutes(
return c.json({ error: 'GitHub device auth is disabled' }, 404)
}
if (remoteMode) {
- return c.json({ error: 'GitHub device auth is managed by the broker in remote mode' }, 403)
+ return c.json({ error: 'GitHub device auth is managed by the server in remote mode' }, 403)
}
try {
@@ -225,7 +225,7 @@ export function createAuthRoutes(
return c.json({ error: 'GitHub device auth is disabled' }, 404)
}
if (remoteMode) {
- return c.json({ error: 'GitHub device auth is managed by the broker in remote mode' }, 403)
+ return c.json({ error: 'GitHub device auth is managed by the server in remote mode' }, 403)
}
const json = await c.req.json().catch(() => null)
diff --git a/hub/src/web/server.ts b/hub/src/web/server.ts
index 2141fee..7d67448 100644
--- a/hub/src/web/server.ts
+++ b/hub/src/web/server.ts
@@ -109,7 +109,7 @@ function createWebApp(options: {
return await next()
}
if (!c.req.header(BROKER_SESSION_HEADER)) {
- return c.text('Broker session required', 401)
+ return c.text('Server session required', 401)
}
return await next()
})
From ed0ef223dd88fe3d5944eecc4aa5fc81c1087577 Mon Sep 17 00:00:00 2001
From: Balamurugan Marimuthu <246387390+bmarimuthu-nv@users.noreply.github.com>
Date: Fri, 1 May 2026 20:00:49 -0700
Subject: [PATCH 3/3] Improve README onboarding structure
Signed-off-by: Balamurugan Marimuthu <246387390+bmarimuthu-nv@users.noreply.github.com>
---
README.md | 113 +++++++++++++++++++++++++++++-------------------------
1 file changed, 61 insertions(+), 52 deletions(-)
diff --git a/README.md b/README.md
index 779b4da..df250fd 100644
--- a/README.md
+++ b/README.md
@@ -6,34 +6,6 @@
Run AI coding sessions locally, then control them from your browser or phone.
-## Start Here
-
-Pick the setup that matches where your sessions will run:
-
-| Use case | Best fit | Open the UI from |
-|----------|----------|------------------|
-| Local laptop or devbox | Hub and sessions on the same machine | The URL printed by `maglev hub start` |
-| SSH workstation | Hub on the remote machine, browser through an SSH tunnel | The forwarded URL on your laptop |
-| Slurm/HPC node | Server on a reachable login/VNC node, hub inside the allocation | The URL printed by `maglev hub start --remote` |
-
-## Direct vs Server Mode
-
-Maglev does not guess whether your browser can reach a hub. You choose the mode when starting the hub.
-
-Use direct mode when your browser can reach the hub URL directly, including through an SSH tunnel:
-
-```bash
-maglev hub start --name devbox
-```
-
-Use server mode when the hub runs somewhere your browser cannot reach directly, such as inside a Slurm allocation or container:
-
-```bash
-maglev hub start --name "slurm-${SLURM_JOB_ID:-manual}" --remote
-```
-
-In server mode, the hub still listens on a local port, but it also registers with `maglev server`. The server URL is discovered from `~/.maglev/server-url` written by `maglev server`, from saved settings, or from `--server-url `.
-
## Install
Fast path: install the latest prebuilt release for your machine.
@@ -61,40 +33,44 @@ If `maglev` is not found after install:
export PATH="$HOME/.local/bin:$PATH"
```
-## Build From Source
+## Main Features
-Use this path when working from a checkout, testing unreleased changes, or using a platform without a release artifact.
+- Persistent session list that survives browser disconnects and Maglev backend restarts.
+- Auto-connect browser sessions back to running terminals when the page or backend reconnects.
+- Auto-respawn terminals when the Maglev backend restarts, with optional per-terminal startup commands.
+- Code diff and review views for inspecting changes and leaving comments.
+- File browser support for each session, including sessions running inside worktrees.
+- Worktree support for creating isolated coding sessions from the web UI.
-Prerequisites:
+## User Workflows
-- `git`
-- `bun`
-- Optional but recommended: `rg` and `difft`
+Pick the setup that matches where your sessions will run:
-```bash
-git clone https://github.com/bmarimuthu-nv/Maglev.git maglev
-cd maglev
-./install.sh
-maglev --version
-```
+| Use case | Best fit | Open the UI from |
+|----------|----------|------------------|
+| Local laptop or devbox | Hub and sessions on the same machine | The URL printed by `maglev hub start` |
+| SSH workstation | Hub on the remote machine, browser through an SSH tunnel | The forwarded URL on your laptop |
+| Slurm/HPC node | Server on a reachable login/VNC node, hub inside the allocation | The URL printed by `maglev hub start --remote` |
-`./install.sh` builds the standalone binary from source and installs `maglev` to `$HOME/.local/bin`.
+### Direct vs Server Mode
-Common variants:
+Maglev does not guess whether your browser can reach a hub. You choose the mode when starting the hub.
+
+Use direct mode when your browser can reach the hub URL directly, including through an SSH tunnel:
```bash
-# Install somewhere else
-MAGLEV_INSTALL_DIR="$HOME/bin" ./install.sh
+maglev hub start --name devbox
+```
-# Force dependency reinstall before building
-FORCE=1 ./install.sh
+Use server mode when the hub runs somewhere your browser cannot reach directly, such as inside a Slurm allocation or container:
-# Build only, without installing
-bun install
-bun run build:standalone
+```bash
+maglev hub start --name "slurm-${SLURM_JOB_ID:-manual}" --remote
```
-## Local Setup
+In server mode, the hub still listens on a local port, but it also registers with `maglev server`. The server URL is discovered from `~/.maglev/server-url` written by `maglev server`, from saved settings, or from `--server-url `.
+
+### Local Setup
Use this direct-mode setup when your browser and coding environment are on the same machine.
@@ -118,7 +94,7 @@ Then open `http://localhost:3006`.
Create sessions from the web UI. `maglev hub start` also starts the local runner for that hub, so you do not need to run `maglev shell` or `maglev runner start` manually for the normal flow.
-## SSH Setup
+### SSH Setup
Use this direct-mode setup when Maglev runs on a remote workstation and your browser reaches it through an SSH tunnel.
@@ -156,7 +132,7 @@ ssh -L 3006:127.0.0.1:3006 user@devbox
Create sessions from the web UI after the runner appears in the machine list.
-## Slurm / HPC Setup
+### Slurm / HPC Setup
Use this server-mode setup when sessions run on ephemeral compute nodes that your browser cannot reach directly.
@@ -193,6 +169,39 @@ maglev server --public-url https://your-reachable-server.example
If Linux user services are not available on the login/VNC/jump node, keep `maglev server` running in a terminal or under your site's preferred process manager.
+## Build From Source
+
+Use this path when working from a checkout, testing unreleased changes, or using a platform without a release artifact.
+
+Prerequisites:
+
+- `git`
+- `bun`
+- Optional but recommended: `rg` and `difft`
+
+```bash
+git clone https://github.com/bmarimuthu-nv/Maglev.git maglev
+cd maglev
+./install.sh
+maglev --version
+```
+
+`./install.sh` builds the standalone binary from source and installs `maglev` to `$HOME/.local/bin`.
+
+Common variants:
+
+```bash
+# Install somewhere else
+MAGLEV_INSTALL_DIR="$HOME/bin" ./install.sh
+
+# Force dependency reinstall before building
+FORCE=1 ./install.sh
+
+# Build only, without installing
+bun install
+bun run build:standalone
+```
+
## Daily Commands
```bash