Skip to content

Upgrading

JohnConnorNPC edited this page Oct 22, 2026 · 11 revisions

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.


What 1.0 promises

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: /state blobs 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.


Python 3.10 is the minimum (1.0)

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:

  1. Stop the agent and the broker (under the shipped units, systemctl stop webterm-agent webterm-broker), then delete .venv (or the directory $VENV_DIR names).
  2. If python3 --version reports 3.10 or newer, start the broker first and let run-broker.sh create the venv, then start the agent. Whichever script runs first creates it with python3, and a venv that run-agent.sh created holds only websockets, so the broker then cannot import sanic.
  3. If python3 is 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.


Terminals no longer inherit the MCP tokens (#260)

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.


A chosen token needs 16 or more characters (#260)

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.


The peer proxy needs websockets 15 (#252)

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.


A peer's MCP token must differ from the hub's (#261)

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.


Broker roles: hub and node, and terminals: false (#253)

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 /info gains role ("hub" / "node", derived from serve_ui) and terminals; GET /mcp/info gains terminals. Clients that ignore unknown keys are unaffected.
  • A node's GET / answers {"ui": false, "role": "node"} instead of {"ui": false}. A health probe that checks ui === false keeps 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.


MCP-launched windows start in readwrite (#243)

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.


AI status checks need a grant on the broker (#189)

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).


The admin token guards routes, not files (#191)

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.


A mod package must declare what it uses (#193)

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.


An installed stylesheet may not use a CSS escape (#260)

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.


The Agent docs button is gone (#177)

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.


The full CSP is Report-Only at 1.0 (#190)

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.


Publishing passwords needs https, localhost or encryption off (#175)

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.


Workspaces became a mod, and the layout blob changed shape (#148)

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.

What changed

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.

What it costs you

Nothing, on either path:

  • Upgrading. The first getLayout() concatenates your existing workspaces' columns into columns, 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.

Downgrading

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.


The token travels in an Authorization header (#144)

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.


A token is required on every connection, including loopback (#142)

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.

What changed

Browserland used to treat loopback as an auth exemption. Three problems:

  1. With no auth_token configured — the shipped default — /sessions, /profiles, /ws and /control were gated by a check that did nothing when no token existed. On a 0.0.0.0 or 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.
  2. Loopback never meant "same machine." The recommended topology is tailscale serve in front of a 127.0.0.1 bind — 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.
  3. /browserland was 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 dial ws://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.

Does this affect me?

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.

Getting your token

python -m webterm.broker --print-token

Prints 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:

  1. $WEB_TERMINAL_TOKEN
  2. auth_token in broker_config.json
  3. webterm_token.json beside the state store
  4. (broker only) mint a new one into that file

The one unavoidable loss: stranded terminals

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_token in 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.

Other behaviour changes

  • 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-token instead, 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_TOKEN before spawning your shell, so echo $WEB_TERMINAL_TOKEN is empty. Shell-side automation that needs it should read webterm_token.json or 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 in Referer), plus X-Frame-Options: DENY and Content-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_token and launch_disabled_no_token 403s are gone. Everything answers 401 auth_required, which is what the UI and the mods already match on.

File permissions

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.

Clone this wiki locally