Repository navigation
Upgrading
What 1.0 supports and promises, then the breaking changes, what they cost you, and how to recover, newest first. Coming from 0.8.0, read the entries from Python 3.10 is the minimum down to Publishing passwords needs https, localhost or encryption off; the ones below those already shipped in 0.8.0.
1.0 supports Windows and Linux, Python 3.10 or newer, and current Chrome, Edge and Firefox.
Within 1.x:
-
Mods: the mod API stays at
ctxVersion: 1, with additions only. Check that a newer member exists before you use it (Writing a Mod). -
Saved state:
/stateblobs written by a 1.x release are read by every later 1.x release. Going back to an older release is not covered. - MCP tools: tool names stay, and their arguments only gain optional fields.
A change that would break one of these three waits for 2.0. Anything not listed here can change in a 1.x release.
What changed. pyproject.toml now has requires-python = ">=3.10" (it was
>=3.9), so pip install -e . refuses a 3.9 interpreter.
Who is affected. Anyone running the broker or the agent on Python 3.9. The
mcp extra already needed 3.10.
What to do. Install Python 3.10 or newer, then reinstall from the checkout
(pip install -e . plus your extras). The launcher scripts run the checkout
with whatever python or python3 is on PATH and never check its version.
On Linux, run-broker.sh and run-agent.sh keep the .venv they created, so
it stays on the old Python until you rebuild it:
- Stop the agent and the broker (under the shipped units,
systemctl stop webterm-agent webterm-broker), then delete.venv(or the directory$VENV_DIRnames). - If
python3 --versionreports 3.10 or newer, start the broker first and letrun-broker.shcreate the venv, then start the agent. Whichever script runs first creates it withpython3, and a venv thatrun-agent.shcreated holds onlywebsockets, so the broker then cannot importsanic. - If
python3is older, create the venv yourself with a 3.10 interpreter from the checkout —python3.10 -m venv .venv, then.venv/bin/pip install -e .— and the scripts use it as it is.
Update to 1.0 by hand, once. A 0.8.0 broker has no Update window, so
update it by hand: git pull, pip install -e . plus your extras, then
restart it. A broker on a later pre-1.0 build that already has the Update
window refuses this update from Update…, because it changes
pyproject.toml ("This update changes dependency files -- pyproject.toml --
…"), so update it by hand the same way. Later updates go through the window
again, unless they change a dependency file too.
What changed. A shell the broker starts no longer inherits
WEB_TERMINAL_MCP_TOKEN, WEB_TERMINAL_MCP_ADMIN_TOKEN,
BROWSERLAND_MCP_TOKEN, BROWSERLAND_MCP_ADMIN_TOKEN or
BROWSERLAND_MCP_HOSTS from the broker's environment, as it already did not
inherit WEB_TERMINAL_TOKEN.
Who is affected. An MCP client or script started inside a Browserland terminal that read one of these from the inherited environment. It now starts without it.
What to do. Give that client its token in its own configuration: the
.mcp.json from the Control Panel's Copy .mcp.json, --token-file, or the
variable set in that shell yourself.
What changed. A broker whose browser token is shorter than 16 characters
refuses to start, with a message naming where the token came from:
$WEB_TERMINAL_TOKEN, auth_token in broker_config.json, or a hand-written
webterm_token.json beside the state store. A token the broker mints itself is
always long enough. The MCP token has no such floor yet; that is a hardening
item, not part of this change.
Who is affected. Only an operator who set a short token themselves. Separately, a wrong token's 401 now arrives one second late (at most 32 at once), so a browser still holding a rotated token waits a second on each poll until you log in again.
What to do. Set a longer value everywhere it is used (each browser's login,
each host entry, BROWSERLAND_MCP_HOSTS peers), or remove it and let the broker
mint one; python -m webterm.broker with --print-token prints it.
A broker that relays to peers now needs websockets 15 or later as well as
httpx (pip install -e ".[proxy]" from the checkout installs both): with an older
websockets its whole /peer/* prefix answers 503 proxy_unavailable, and
the startup line names the missing version. Brokers without peers are
unaffected. See Technical-Reference#peer-proxy-252.
What changed. The hub refuses to pass any of its own tokens (MCP, browser,
MCP admin or broker admin) to a peer: a call on a /peer/<id>/mcp/* entry
whose bearer, or X-Browserland-Mcp-Admin header, equals one of them answers
403 hub_credential_refused (for a peer that exists and has mcp: true; an
unknown or not-opted-in peer answers unknown_peer instead) and nothing is
forwarded. Before, the hub relayed whatever bearer it was given.
Does this affect me? Only if a hub and a peer share one MCP token and an
MCP client's peer entry (http://<hub>/peer/<id>) carries it. That setup
worked before and is refused now, on purpose: with one token on both brokers a
wrong paste hands the hub's token to the peer.
What to do. Give the peer its own MCP token (Control Panel → MCP access on the peer, Generate), different from the hub's, and put it in the peer entry. Nothing else changes: a peer token that only looks like a hub token (a shared prefix, say) still relays; only an exact match is refused. See MCP-and-AI-Agents#a-broker-behind-another-one-252.
Hub entries with peers. A hub entry may now carry its peers' tokens (see
MCP-and-AI-Agents#a-broker-behind-another-one-252); existing configs are
unchanged. A hub entry named local with peers.b, next to Copy's own
local.b entry, is refused when the MCP server starts (the names collide): keep
one of the two.
Nothing changes for an existing deploy. A config without the new terminals
key keeps launching terminals exactly as before, and serve_ui keeps meaning
what it meant: true is now called a hub, false (or --headless) a
node. Two things are additive and visible:
-
GET /infogainsrole("hub"/"node", derived fromserve_ui) andterminals;GET /mcp/infogainsterminals. Clients that ignore unknown keys are unaffected. -
A node's
GET /answers{"ui": false, "role": "node"}instead of{"ui": false}. A health probe that checksui === falsekeeps working; one that compares the whole body for equality must accept the new key.
The new behaviour is opt-in: terminals: false (or --no-terminals) makes a
hub refuse new launches with terminals_disabled. A node with
terminals: false refuses to start. See
Technical-Reference#broker.
What changed. A window an MCP client launches (launch_terminal,
POST /mcp/launch) comes up in the launch's mode, or in readwrite when it
passes none — scoped or unscoped alike. An unscoped launch used to inherit the
broker's default_mode. Every launch also mints a launch handle that lets the
launching agent change that window's scope and mode later (set_window,
POST /mcp/window).
Who is affected. A broker with allow_launch on whose operator relied on
default_mode (say read) to cap what agents can do in the terminals they
launch unscoped and without a mode: those terminals are now drivable by
their launcher. Scoped launches already started in readwrite. With
allow_launch off (the default) nothing changes.
What to do. If launched windows must start read-only, have the agent pass
mode: "read", or turn allow_launch off. Setting the window's mode from its
robot button only revokes write access after the launch (the window comes up
drivable first); a browser edit also clears the launch handle, so the agent
cannot change it back.
What changed. The broker's /status/fetch, the only way the AI status mod
reaches the providers' status pages, is behind a new gate,
status_fetch_enabled, which is off by default. While it is off the route
answers 503 status_fetch_disabled.
Who is affected. Anyone who had the AI status mod on: its chip and window now say status checks are switched off on this broker instead of showing the providers.
What to do. Click Switch on status checks on this broker in the AI
status window, or put "status_fetch_enabled": true in that broker's
broker_config.json (a key in the file owns the gate from then on).
What changed. Nothing in the code. 1.0 states a limit that was only implied
before: the admin_token you can set in the broker's config makes install,
uninstall, rescan and the two policy writes need the X-Webterm-Admin header,
but it does not stop a holder of the page token from writing files into
mods_dir through /file/*, and a broker that serves the UI scans mods_dir
at its next boot and serves whatever validates there, with no admin header.
Does this affect me? Only if you rely on admin_token to keep a page-token
holder (or an installed mod) from getting code served. It keeps the install
routes apart from the page token; it is not a boundary around mods_dir.
What to do. Treat the page token as able to change what the next boot
serves, and keep mods_dir as protected as the broker's config. A check that
refuses /file/* writes inside mods_dir is not part of 1.0. See the Mods
section of Technical-Reference.
What changed. Installing a mod checks its scripts against the
permissions list in its mod.json. A package that uses a capability it does
not declare is refused with 400 undeclared_capability, and a manifest with no
permissions key counts as declaring none. Mods this broker already installed
keep loading; the check runs on every new install.
Who is affected. Anyone installing a package written for 0.8.0 whose
manifest has no permissions list, or an incomplete one — for example when
installing it on a second broker.
What to do. Add what the package uses to permissions in its mod.json;
the refusal names the file, the line and the permission it needs. See
Writing a Mod.
What changed. A .css file in an installed mod may not contain a
backslash outside a comment: a CSS escape such as @\69mport or
url(h\74tp://…) hid an external reference from the install check. Install
refuses such a package with 400 css_external_reference, and the boot scan
applies the same rule. Unlike the #193 permissions check, mods installed before
the upgrade are not exempt: one that uses an escape stops loading at the next
boot, and its row is gone from the Mods pane.
Who is affected. Only a broker with an installed mod (an x- id) whose
stylesheet uses a backslash outside a comment, for example
content: "\2014". Shipped mods are not affected.
How to see it. The broker logs mods_dir: skipping '<id>' -- css_external_reference: … at start, naming the file, and
GET /mods/installed lists the mod under skipped with that code.
What to do. Replace each escape with the character itself (the file is
UTF-8), or with a relative path or data: URI where it spelled a URL, and
install the fixed package again. See Writing a Mod.
What changed. The Agent-docs mod, the 📋 button on every terminal's title
bar that opened that folder's AGENTS.md and CLAUDE.md, is retired. It
guessed the folder from the terminal's detected working directory, so it could
open — and save — another project's file.
Who is affected. Anyone who used the 📋 button. Agent-docs windows you already have keep restoring and saving.
What to do. Open AGENTS.md and CLAUDE.md with the text editor or the
file manager. Bringing the mod back is a source-tree change, described in
webterm/broker/mods-deprecated/README.md.
What changed. Nothing new is enforced. The policy a browser enforces is
still the narrow one: script-src (the broker's own scripts, with a hash for
the page's one inline script) and frame-ancestors 'none'. The fuller policy is
sent on every response as Content-Security-Policy-Report-Only at 1.0: the
browser reports what it would block and blocks nothing. Enforcing it waits for
a re-soak in the deployed shape (https and wss behind a proxy).
Does this affect me? Only if you count on the CSP to restrict more than scripts and framing. It does not, yet.
What to do. Nothing. Enforcing the fuller policy is planned after a re-soak in the deployed shape; no release is promised.
What changed. The Broker registry encrypts what it publishes in the
browser, and its default, Passwords only, needs a secure page (https, or
localhost). On a plain-http page such as http://192.168.1.5:4445, a Publish
with Include passwords ticked is refused ("Publish cancelled. Broker
registry encryption is set to …") instead of writing the tokens in the clear.
Who is affected. Anyone who published passwords from a plain-http page. Publishing without passwords is unchanged.
What to do. Reach the broker over https (for example tailscale serve) or
on localhost, or set Control Panel → Browser → Broker registry encryption to
Off — publish in the clear, which is the old behaviour.
Nothing to do, and nothing is lost. This is here because the persisted layout changes shape on first load, and because the feature can now be turned off.
prefs._layout used to group tiled columns under workspaces:[...] with an
activeWs index, and the tiling core owned that model. It is now a single
desktop:
before { mode, activeWs, workspaces:[ {id, name?, focusedCol, columns:[...]} ] }
after { mode, focusedCol, columns:[...],
mods: { workspaces: { version:1, activeId, list:[{id,name?,focusedCol,focusedColId?,focusedColIdAt?}] } } }
Every column lives in columns exactly once. Workspaces are a mod
(mods/workspaces/, enabled by default) that stamps a wsId on each
column and tells the strip which ones to draw.
Nothing, on either path:
-
Upgrading. The first
getLayout()concatenates your existing workspaces' columns intocolumns, in order, and hands the grouping to the mod, which rebuilds your workspaces from it. Names and the workspace you were last on are preserved. The migration is one-way and idempotent, and it is non-destructive — no column is dropped even if the mod never loads — so a browser that migrates first cannot damage a peer that has not yet. - Turning the mod off. Every column from every workspace appears together on one desktop; the pager, the nine workspace shortcuts and the workspace menu items go away. Your grouping is kept, not deleted: turn it back on and the workspaces return exactly as they were. The same is true of pinning it off broker-wide (Control Panel → Advanced → Mods on this broker).
Your keyboard bindings for workspace-prev / workspace-next /
workspace-1..5 are stored by action id and are untouched by any of this.
An older build reads the new blob as a layout with no workspaces key, which
its reconcileLayout heals into one workspace holding no columns — your
windows come back as floating windows rather than tiled, and you would rebuild
the layout. Keep a copy of webterm_state.json if you plan to roll back.
What changed. The page sends the broker token as
Authorization: Bearer <token> on every HTTP call, never in the query string.
Only WebSocket dials (a browser cannot set headers on them) and the first
?token= deep link still carry it in a URL, and the deep link is scrubbed from
the address bar on load. Cross-origin requests may now need OPTIONS
preflights, which a browser caches; the broker advertises a maximum age of
86400 seconds (Access-Control-Max-Age).
Who is affected. Anyone fronting a broker with a reverse proxy that strips
or rewrites the Authorization header, or that does not pass OPTIONS
through: every host behind it then looks like a wrong password, and the login
overlay keeps coming back. Scripts calling a broker's own routes are
unaffected — those still accept ?token= and ?auth= as well as the header —
but the /peer/<id>/… relay (#252) takes the hub's token in the
Authorization header only.
What to do. Make the proxy forward the Authorization header unchanged and
let OPTIONS requests through. See Technical-Reference#auth-model.
This breaks tokenless installs. It cannot not break them — the whole point is that a request which used to be accepted is now refused. Read the table below to find your case.
Browserland used to treat loopback as an auth exemption. Three problems:
-
With no
auth_tokenconfigured — the shipped default —/sessions,/profiles,/wsand/controlwere gated by a check that did nothing when no token existed. On a0.0.0.0or tailnet bind that was an open read of every terminal's title, pid and cwd, an open attach to every PTY, and an open steal of the single-active-browser lease. -
Loopback never meant "same machine." The recommended topology is
tailscale servein front of a127.0.0.1bind — and a proxied request arrives from loopback. So the recommended deployment handed the whole tailnet an exempt path to/launch(shell spawn) and/file/*(host-wide read/write). A page in your own browser reaches loopback too. -
/browserlandwas exempt even when a token WAS configured — the one gate the token never covered. WebSockets are not CORS-gated, so any website you had open could dialws://127.0.0.1:4445/browserland, re-register a live window id (kicking the real agent off) and inject fabricated output into a terminal you trust.
Now: a token is required on every surface, on every interface, always. There is no loopback exemption and no opt-out.
The only unauthenticated responses are GET / (the token is typed into that
page, and auth is query/header-only with no cookies — gating the document would
401 every reload, bookmark and new tab forever), GET /help-corpus.json, and
the OPTIONS preflights, which carry no credentials by design — plus two added
later, GET /vendor/* (#143) and GET /mods/<id>/<gen>/<name> (#163), which a
<script src> must load before any token exists. /mcp/* keeps its own
separate mcp_token realm. See Technical-Reference#auth-model for the
current table.
A fresh install still works with one command. With nothing configured the
broker mints its own token into webterm_token.json beside its state store and
prints a ready-to-open ?token=… URL.
| Your install | What breaks | Recovery |
|---|---|---|
| Tokenless, loopback only (the shipped default) | The browser asks for a token on next load. Terminals launched before the restart are stranded. Scripts curling /sessions, /file/* or /launch over loopback now get 401. |
The token is printed at startup and by --print-token. Relaunch the terminals. |
Tokenless behind tailscale serve / a proxy |
Same, plus every remote browser must enter the token once. | Same. |
auth_token already configured |
Almost nothing. Running agents already carry WEB_TERMINAL_TOKEN and reconnect fine; browsers already stored it. Only hand-started local agents and tokenless local scripts break. |
launchers/run-agent.{sh,ps1} now read the sidecar automatically, or set $WEB_TERMINAL_TOKEN. |
MCP clients (webterm.mcptool) |
Nothing. Separate mcp_token, Authorization: Bearer, /mcp/* realm — untouched. |
— |
Node (serve_ui: false, --headless) |
Nothing. GET / still answers 200 with "ui": false, so health probes keep working. Since #253 the body also names the role — {"ui": false, "role": "node"} — which a probe that checks ui === false still reads as "no page". |
A probe that compares the whole body for equality needs the new key. |
python -m webterm.broker --print-tokenPrints the token and the URL to open, without starting a server. It never mints one — asking for the token must not be the thing that creates it.
Resolution order, for the broker and for --print-token alike:
$WEB_TERMINAL_TOKEN-
auth_tokeninbroker_config.json -
webterm_token.jsonbeside the state store - (broker only) mint a new one into that file
If you were running tokenless, the terminals that broker launched were
spawned with WEB_TERMINAL_TOKEN deliberately removed from their environment.
After you restart the broker they cannot re-register.
Their shells keep running. Their windows never come back. There is no server-side rescue: an environment variable cannot be injected into a live process. The broker notices this happening and, after a few refused producer connections, logs what it means and points here.
So, before deploying:
- Finish or accept the loss of whatever is running in those terminals.
- Consider setting an explicit
auth_tokenin your config before the cutover, so you know the value in advance rather than discovering it in the log afterwards. - Then restart the broker and relaunch your terminals once. From then on everything self-heals: terminals launched by the UI get the token from the broker automatically.
-
The startup banner prints the full
?token=…URL only on the run that minted it, and only to an interactive terminal. Under systemd, Docker or CI it prints a pointer to--print-tokeninstead, so a live credential is not baked into aggregated logs. Sanic's access log is now off for the same reason (the token rides in the query string). -
A first start against a directory that already holds broker state prints a
loud
UPGRADE NOTICE— that combination means this install used to run tokenless. -
The token is no longer visible from inside your terminals. The agent
removes
WEB_TERMINAL_TOKENbefore spawning your shell, soecho $WEB_TERMINAL_TOKENis empty. Shell-side automation that needs it should readwebterm_token.jsonor run--print-token. -
An agent refused for a missing token retries every 10 s instead of once a
second, and logs one error naming
$WEB_TERMINAL_TOKEN. It does not give up permanently — restoring the right token lets it reconnect on its own. -
New response headers:
Referrer-Policy: no-referrer(the desktop URL carries the token, so an outbound link must not leak it inReferer), plusX-Frame-Options: DENYandContent-Security-Policy: frame-ancestors 'none'(GET /is public so login can bootstrap, but it must not be embeddable — an attacker page cannot read across origins, yet could otherwise clickjack a browser that already holds a token). -
Error shapes: the
disabled_no_tokenandlaunch_disabled_no_token403s are gone. Everything answers401 auth_required, which is what the UI and the mods already match on.
webterm_token.json is created 0600 on POSIX. Windows has no POSIX file
mode: the file inherits the containing directory's ACL, so on a shared
Windows host make sure the broker's working directory is not readable by other
accounts. The file is git-ignored either way.
If the directory is not writable the broker still starts, but with an
ephemeral token: it changes on every restart, --print-token cannot
recover it, and terminals do not survive a restart. That case warns loudly on
every start.
Browserland — a web-based terminal desktop. This wiki is the end-user guide; for the wire protocol, auth model, and HTTP/MCP contract see Technical Reference. Released under the MIT License.
Start here
Building layouts
The desktop
Windows & content
Multi-host & AI
For developers