Administration - kubed-io/selenium-flow GitHub Wiki

Administration

The token, the admin UI, and the files a workspace keeps.

The token

Setting AUTH_TOKEN turns on authentication for both surfaces at once. Leave it unset and the server is open โ€” correct for docker compose up, and the reason a real deployment always sets one.

It is not only a credential. The same value is the signing key for file links and the event stream, so rotating it revokes every link already handed out. That is deliberate: it means there is one secret to manage rather than three, and one action that invalidates everything.

Making one

Any long random string works. What matters is that it survives being pasted into a client config, so keep it alphanumeric:

openssl rand -hex 32

In Kubernetes, don't. Let External Secrets generate it so no value is ever authored, committed, or staged in a vault โ€” see Deployment. To read the current value:

kubectl get secret selenium-flow-auth -n flow \
  -o jsonpath='{.data.AUTH_TOKEN}' | base64 -d

Using it

Authorization: Bearer <token>

Both surfaces accept that. The HTTP endpoints additionally accept the bare token as the whole header value, for clients that cannot express a scheme.

GET /health and GET /openapi.yaml are always open. A kubelet has no token, and a contract you must authenticate to read is needlessly awkward.

Rotating it

  1. Replace the secret. In Kubernetes, delete it and let the generator remake it: kubectl delete secret selenium-flow-auth -n flow
  2. kubectl rollout restart deploy/selenium-flow -n flow โ€” envFrom does not reload, so the pod keeps the old value until it restarts.
  3. Update every client. Anything holding the old token now gets a 401, and every signed file link stops working.

The admin UI

GET / โ€” Workspaces, with each one's Files, Flows, History and Site data; Secrets and Settings, read-only; and the Grid's own console, framed alongside the others.

There are no accounts. The sign-in box asks for the server's token, because anyone holding it can already drive every browser through the API; a second identity system would only be one more thing to get wrong. It is kept in sessionStorage, so it does not outlive the tab you typed it into.

With oidc.client_id set, the card also offers Sign in with OIDC: the page signs in with the issuer itself and keeps the tokens in memory, so a reload signs in again, silently while the issuer's session lasts. It needs an admin role (oidc.admin_roles). Inside a frame the button opens the UI in a new tab.

What the list tells you

These are workspaces, not Grid sessions. The Grid is the superset โ€” it runs browsers put there by anything at all โ€” and those are deliberately not listed: showing them would present somebody else's work as though it were ours. The Grid console tab is there if you want to see the Grid itself.

A workspace is a caller and its context. It may or may not have a session โ€” a live browser โ€” open right now:

Row shows Means
a name keyed with ?workspace=<name> or X-Workspace
stdio the one client of a stdio server, which names nothing
live a session is open: a browser is attached and the Grid still has it
idle no session open โ€” reaped, or ended here

An idle workspace is not broken. It kept the browser it was using and the page it was on, so its caller's next open_session picks all of that back up.

Workspaces expire on WORKSPACE_TTL, slid forward on every use โ€” a day by default. One in daily use never goes; one abandoned yesterday is already gone. There is no delete button on purpose.

Each row is also marked with the browser it is running โ€” ๐ŸŸข Chrome, ๐ŸฆŠ Firefox โ€” read from the capabilities the Grid reports, so it is the browser actually running rather than the one that was asked for.

The list pushes its own updates over Server-Sent Events, so there is no refresh button. One poll loop on the server feeds every open page, rather than each tab polling on its own.

Ending a session by hand

Click into a workspace and its toolbar carries End browser. It ends the session: quits the browser and gives its Grid slot back immediately, and is unavailable when the workspace is already idle. Clearing files is a separate action, one button per section inside the Files tab below โ€” see Files and Flows.

The list rows carry no buttons on purpose: an action inside a card sits next to the status pill and makes that pill look clickable too.

It does not delete the workspace. The record stays, with its browser choice and last page, and the row goes idle. That is the difference that makes this safe to click: the caller loses the browser's live state โ€” a login, a half-filled form โ€” but not its place, and its next open_session carries on.

You do not have to click it at all. An abandoned browser expires on its own: the Grid reaps it after SE_NODE_SESSION_TIMEOUT seconds idle (300 by default), and where the browser nodes autoscale, the node then scales back to zero. The button is for not waiting out those minutes while a slot sits held โ€” the Grid runs a handful of browsers in total, so a stale one is capacity somebody else needs.

The MCP surface has none of this

An MCP client gets open_session and end_browser, and can only ever touch its own workspace. Nothing there lists workspaces, because a listing would hand any client the names of everybody else's โ€” and a name is the address of a browser it could then drive with the same token. This page describes the admin surface: HTTP and UI, gated on the server token, or an admin-UI sign-in.

Endpoints behind it

Route Auth Returns
GET / none โ€” it is the sign-in form the page (/admin redirects here)
GET /admin/workspaces bearer the list, plus a signed events_url
GET /admin/events signature the list again, whenever it changes
DELETE /admin/workspaces/{key}/session bearer ends its session (the browser); the workspace is kept
GET /admin/workspaces/{key}/files bearer the sections at once: Downloads, Screenshots, Recordings, Files
POST /admin/workspaces/{key}/files/{screenshots|recordings|downloads}/{name}/keep bearer moves or copies one into Files
DELETE /admin/workspaces/{key}/files/downloads bearer clears Downloads โ€” the Grid's whole store
DELETE /admin/workspaces/{key}/files/screenshots bearer clears Screenshots (recordings likewise)
DELETE /admin/workspaces/{key}/files/{name} bearer deletes one file from Files
GET /admin/secrets bearer the secrets catalogue
GET /files/{session_id}/{name} signature one download, keyed by the browser's own id
GET /screenshots/{workspace}/{name} signature one screenshot, keyed by the workspace name
GET /kept/{workspace}/{name} signature one file in Files, keyed by the workspace name

DELETE .../files/{name} is the one route two things happen behind: name being the literal downloads or screenshots clears that whole section, anything else deletes that one file from Files โ€” because those two words are themselves reserved names Files can never hold, the same rule keep_file follows (ยงF4.6).


Files and Flows

Opening a workspace shows its Files tab by default, with Flows beside it โ€” the hash records which one, #/workspaces/<key> and #/workspaces/<key>/flows, so either is a link you can send someone.

Files is four rows, in a fixed order, each cleared the way that fits what it holds โ€” see Files for the full shape of the four:

  • Downloads โ€” whatever the site served. Clear downloads deletes the Grid's whole store for this browser, and is disabled unless the browser is live: an idle workspace has nothing left on the Grid to clear. Every tile also carries a keep button, visible on hover, that copies it into Files โ€” the original stays on the Grid.
  • Screenshots โ€” every screenshot, newest first. Clear screenshots deletes all of them in one step; anything already kept is in Files and is untouched. Its tiles carry the same keep button, and here it moves the file into Files instead.
  • Recordings โ€” the video of each browser opened with record=true, filed shortly after it ends. Clear recordings and the keep button work as they do for Screenshots; a tile opens the video in the lightbox, and a browser being recorded carries a REC mark in the list.
  • Files โ€” prints, and anything kept from the three rows above. No clear button: everything here was put there on purpose, so there is only a per-file delete, on each tile.

This needs SE_NODE_ENABLE_MANAGED_DOWNLOADS=true on the Grid node for Downloads, and DATA_DIR for Screenshots, Recordings and Files โ€” see Deployment.

The lightbox

Clicking a tile's thumbnail opens a lightbox over that row's own grid rather than a merge of all three, with โ€น Prev and Next โ€บ, the same through the โ—€ / โ–ถ arrow keys, disabled at either end, and Esc to close. Keep, inside the lightbox, moves straight on to the next screenshot rather than closing โ€” useful for working through a whole batch without reopening it each time.

Sharing one

A file is often wanted somewhere that cannot present a token: an <img> tag, a markdown image in a chat transcript, a link to a colleague. Browsers do not attach an Authorization header to an image request, so those URLs are authorised by signature instead, one route per section โ€” see the endpoint table above:

/kept/{workspace}/{name}?exp=1788931047&sig=f2235d5d072cea12โ€ฆ

The signature covers that exact path and that expiry, so a link grants one file for a while rather than access to the API, and a recipient cannot extend their own link by editing the query. workspace://files and the admin UI both hand these out already signed; nothing needs to construct one.

Three ways this is normally used:

  • an agent that cannot display an image returns the link, and a chat that renders markdown shows the picture
  • the admin UI uses them as <img src> for thumbnails, and opens images and PDFs in the lightbox rather than a new tab
  • you paste one to somebody who has no token at all

Rotating AUTH_TOKEN invalidates all of them at once.

Later

Screenshots, Recordings and Files live under DATA_DIR/workspaces/<name>/ until kept, cleared or deleted by an operator. A WebDAV backend behind the same interface is the intended answer for somewhere else to keep them, and is not built yet.

Secrets

The Secrets tab (#/secrets) is read-only: a card per secret with its keys and where it may be used โ€” an allow-list of sites, or "any site" โ€” and never a value, because the server has none to send.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ