Repository navigation
2026 10 26 session capabilities design
Status: SETTLED 2026-10-26, issue #82 / PR #87. Rulings 1–22 were read by
Dr K against the thread on #82 and found matching. Rulings 23–31 are the plan
agent's calls, made checking this spec against the code on PR #87, and were
read right on the pull request. Ruling 23's open question was answered there
(Dr K, 10-26), and nothing is open. The plan is
docs/superpowers/plans/2026-10-26-session-capabilities.md. It builds on the
workspaces rename (2026-10-09-workspaces-rename-design.md) and the session
monitor (2026-10-09-session-monitor-design.md). #86 (headless) and #83 (live
view) build on this one. The admin UI is drawn in project-docs,
design/session.md and design/README.md (Penpot file Admin UI). This spec
says only what the UI needs from the server and where the design applies.
Rulings marked Dr K are settled. Rulings marked agent's call were told to Dr K on the issue or the pull request and not objected to.
open_session takes a capabilities object that is sent to the Grid as is,
best effort. On top of it, the capabilities selenium-flow already treats in a
specific way get promoted to named, typed options, with defaults, rules and
messages that steer a model to the right choice. The raw object is the "only if
you really need to" escape hatch. A server-wide default rides under every
request, which is how our Grid's kubed:pool: default reaches it. The admin
UI gets a Session tab with three parts: what the workspace asks for, what
the Grid gave, and every capability sent with where it came from.
It came out of a Chrome 156 crash hunt, which raised three feature requests:
- #1: pick a browser version;
- #3: target one node without hijacking the Grid;
- #4: per-session browser args.
-
Best effort (Dr K, 10-25). selenium-flow sends the caller's capabilities and reports what came back. It never refuses a request because it cannot see a node that would serve it.
-
No catalogue of node kinds (Dr K, 10-25). The Grid knows nothing about nodes scaled to zero, so a list would be static. A static list that is enforced does harm on a Grid that spawns nodes to match its queue.
-
Promote what is already special (Dr K, 10-25). Raw
capabilitiesis for the rest. -
Errors and results guide the model (Dr K, 10-25). A miss explains itself in words a model can act on, and points at where to look.
-
Vendor blocks merge (Dr K, 10-25). For
goog:chromeOptionsandmoz:firefoxOptions, the caller's args are added to ours and its prefs are added too. Where both set the same thing, ours wins. -
The named options (Dr K, 10-26) are:
-
browserandversion; -
widthandheight; -
headless(#86); -
record,captureandinsecure; -
page_load_timeoutandscript_timeout.
Nothing else: no
se:name, nopageLoadStrategy. -
-
Store the inputs, recompute the output (Dr K, 10-26).
- The workspace keeps the named options and the caller's raw
capabilities, and carries them over. - The full set sent is kept with that one session, for display only, and is never reused.
- Config changes and upgrades take effect at the next open.
- Window size and the timeouts are WebDriver commands sent after the browser starts, not capabilities.
- The workspace keeps the named options and the caller's raw
-
A config default
capabilities(Dr K, 10-26). -
Every capability sent has a source (Dr K, 10-26):
flow,config,optionorraw. A merged vendor block can have more than one. -
The per-client layer (Dr K, 10-26).
versionisbrowser's twin, so it gets?version=/X-Versionthe waybrowserhas?browser=/X-Browser. Rawcapabilitiesgets none. There is noX-Capabilities: the config default carries the pool, and nobody has asked for pools per connector. -
The idle WORKSPACE card (Dr K, 10-26) shows only what an explicit
open_session()carries over, so it has no RECORD row and no CAPTURE row. The masthead'scapture pausedpill says the rest.design/session.mdis updated;png/session-idle.pngstill shows the old row. -
Wait as long as the Grid does (Dr K, 10-26), and answer with our own 503 text when it gives up. The reason: a KEDA scale-up from zero takes a minute or more, and giving up first would abandon a request the Grid may still serve. The Grid copes with that case on its own: its distributor stops a session it created after the request timed out or the client went away ("stopping it to avoid stalled browser",
LocalDistributor, 4.48). Nothing here relies on it. -
An explicit
capabilitiesreplaces the remembered object whole, and{}clears it (Dr K, 10-26). Replacing the raw object only ever touches what came from raw. Nothing raw may undo or fight a built-in setting; see Building the sent set. -
A raw key may override a config default (Dr K, 10-26). This is how a call picks another
kubed:pool, and that key's source then readsraw. -
Owned keys are refused (Dr K, 10-26). A raw key that selenium-flow or a named option produces is a 400 naming the option, never applied quietly.
-
A node refusing the session is a 400 only when the open sent something the caller could be at fault for (Dr K, 10-26): raw keys or a
version. The message quotes the node's reason and points at those raw keys or that version as the likely cause. An open that sent neither is refused because of the node itself (a starved node, Chrome failing to start), so that refusal is a 503, which a retry can act on. The queue timeout is always a 503. A refusal and a queue timeout are told apart by the Grid's error text, which this spec names and a test holds. -
sentis what that open sent (Dr K, 10-26). A config change shows at the next open, never on a session that is already open. -
The raw object is typed
dict[str, JsonValue](pydantic), and the owned-key list is the one below (Dr K, 10-26). -
version="latest"clears a remembered version (agent's call), andlatestis never sent. The WORKSPACE card'schrome · latestuses the same word. The Grid's own "any version" is an empty value orstable. -
An explicit
browserdifferent from the remembered one drops a rememberedversion(agent's call). Otherwise Firefox inherits Chrome's 152 and waits 300 s for a node that does not exist. -
session.versionexists in config (agent's call), asbrowser's twin (ruling 10). Without it nothing server-wide could set a version, becausebrowserVersionis an owned key. -
The ENDED wording and the skill's layout are the agent's calls; Dr K delegated both. See Admin API and UI and The skill.
-
The sent set is exactly what the builder returns, and
insecureownsacceptInsecureCerts(agent's call, PR #87; theinsecurehalf is Dr K's, 10-26). Selenium's Options objects add keys of their own today, so the Capabilities section would otherwise not be the truth:-
pageLoadStrategy: normalon both browsers. It is not sent any more. Absent, W3C's default is the samenormal, and a rawpageLoadStrategyis then an ordinary raw key (ruling 6 keeps it unnamed). - On Firefox,
acceptInsecureCerts: trueandmoz:debuggerAddress: true, fromDesiredCapabilities.FIREFOX. So every Firefox browser accepted any certificate, whateverinsecuresaid, and an insecure Firefox browser still got saved site data, whichopen_sessionexists to prevent. The bug was thatinsecureonly ever senttrueand left the rest to Selenium's default. -
The fix (Dr K):
insecurealways sendsacceptInsecureCertsexplicitly,trueorfalse, on every browser, with the sourceoption. It is a standard W3C capability, so the option needs no per-browser translation (unlikeheadless). Firefox then followsinsecureexactly as Chrome does, and the site-data rule holds on both. Anything browser-specific still goes through rawcapabilities. -
moz:debuggerAddress(a CDP address, which AGENTS.md rules out) is not sent. - A Firefox user driving a self-signed site now needs
insecure=true, as on Chrome. There is one user, so there is no compatibility shim; aFixed:changelog line says it.
-
-
Grid-side failures are told apart by message (agent's call, PR #87), answering the question this spec left to the plan. The router's own
SessionNotCreatedExceptionmessages (checked in the 4.48.0 source):Unable to find owning node for slot,Interrupted when creating the session,An error occurred creating the session,No capabilities found in session request payloadandClient has gone away. They live in one constant beside the two queue-timeout strings, a test pins all of them, and a failure carrying one stays the plain 503 it is today. Anything elseSessionNotCreatedExceptionis a node's refusal (ruling 16). Where they came from cannot be told apart: both arrive as the same HTTP 500session not created. -
What the 503 quotes (agent's call, PR #87):
browserName,browserVersion,platformName, and every key with a:outside the never-matched prefixes (goog:,moz:,ms:,safari:,se:). Those are the keys the Grid matches on, so they are the ones that can explain a miss. A scalar value is quoted askey=value; any other value as the key alone. A vendor block,proxyand anything unmatched are never quoted, because a cloud vendor's options and a proxy URL are where credentials live. -
A remembered raw key that an upgrade makes owned (agent's call, PR #87). Rebuilding at a reopen after a reap refuses it with the same 400, which adds: clear it with
open_session(capabilities={}). Dropping it quietly would change the browser's shape, which is what a reopen must never do. -
A raw vendor block of the wrong shape is a 400 (agent's call, PR #87), before anything ends:
argsthat is not a list of strings, orprefsthat is not an object. A merge cannot add to either, and sending it anyway would make a node's refusal out of a typo. -
ignoredlists<block>.prefs.<name>(agent's call, PR #87), and only when a config or raw value differs from the one that won. Args are never ignored: an exact duplicate is dropped, and anything else is added. -
A named option can add a vendor arg through
option_args(agent's call, PR #87): a keyword of the builder that nothing passes yet. It is what #86 needs from this change, and a test proves the merge and theoptionsource with it. -
The admin payload names the asked version
asked_version(agent's call, PR #87). The row'sversionalready exists and is the Grid listing's, as ran, which the list view shows. -
The Session tab is the workspace's address (agent's call, PR #87):
#/workspaces/<key>opens Session, and Files moves to#/workspaces/<key>/files. The integration flowkeep-a-screenshot.yamlnavigates to the new Files hash. No flow is added.
Checked against the source of Selenium Grid 4.48.0, the version the cluster runs. The issue body holds the rest: KEDA's trigger matching, docker-selenium node images, and the headless table.
-
DefaultSlotMatcherdecides which node a request can land on.- An extension capability (one with
:in its name) is matched only when the request carries it. - Capabilities starting
goog:,moz:,ms:,safari:orse:are never matched. - A
browserVersionthat is null, empty orstablematches any node. Anything else is compared bySemanticVersionComparator, so152matches152.0.7977.82.
- An extension capability (one with
-
LocalDistributor.newSessionanswers in one of two ways.- When no node supports the capabilities, or none has a free slot, it raises
RetrySessionRequestExceptionand the request goes back to the front of the queue. - When a node is chosen and its
newSessionfails, the node'sSessionNotCreatedExceptionis returned at once, carrying the node's own message.startSessionadds one message of its own,Unable to find owning node for slot.
- When no node supports the capabilities, or none has a free slot, it raises
-
LocalNewSessionQueuetimes out a request still queued atsessionRequestTimeout(300 s here).- It fails the request with
SessionNotCreatedException("New session request timed out")or("Timed out creating session"). Its other failures areInterrupted when creating the session,An error occurred creating the sessionandClient has gone away. - That arrives as HTTP 500 with
error: session not created. - Python raises it as
SessionNotCreatedException, whicherrors.pyalready maps to 503.
- It fails the request with
-
Python's
ClientConfig.timeoutdefaults tosocket.getdefaulttimeout(), which is None, and this repository sets none.- So
Grid.openalready waits the Grid's whole queue timeout (ruling 12 keeps it that way). - While it waits, it holds one of the 40 worker threads that FastMCP's sync tools and Starlette's routes share.
- So
-
Selenium's Python Options (read in the 4.38 source; the plan's Task 2
tests pin the result against the installed version, 4.48 or later):
-
BaseOptions.__init__starts fromdefault_capabilitiesand setspageLoadStrategy: normal. -
FirefoxOptions.default_capabilitiesisDesiredCapabilities.FIREFOX:browserName,acceptInsecureCerts: true,moz:debuggerAddress: true(ruling 23). -
to_capabilities()rebuilds the vendor block from the object's own args, prefs and experimental options, so a vendor block set withset_capabilityis overwritten. The sent dict has to be whatto_capabilities()returns, not something set into it key by key.
-
-
KEDA scales a node class only for a request that carries every key in
that class's trigger
capabilities. Once every class carries akubed:pool, a request without one scales nothing, which is why the config default exists.
open_session gains two arguments, on the tool (mcp/tools.py), the
endpoint (POST /browser) and the action (core/actions.py):
-
version: str | None: the browser version to ask for, such as152or152.0.7977.82.latestclears a remembered one. -
capabilities: dict[str, JsonValue] | None: extra W3C capabilities, sent as given, best effort.{}clears a remembered object.
headless is #86's to add. What this change owes #86 is that a named option
can add a vendor arg (ruling 29).
The docstring is prompt, so it presents the named options as one coherent set: what each controls, and whether it is remembered. The raw object comes last, as the escape hatch: use it only for a capability no option covers, and expect a key an option controls to be refused with the option named. The existing options are described in the same style.
| Setting | Config (session.*) |
Client (?param / header) |
Carried to an explicit open_session()
|
Replayed after a reap |
|---|---|---|---|---|
browser |
yes |
?browser / X-Browser
|
yes | yes |
version |
session.version (ruling 21) |
?version / X-Version
|
yes; dropped when browser changes explicitly |
yes |
width, height, the timeouts |
yes | yes, as today | yes | yes |
insecure |
no | no | yes, as today | yes |
record, capture
|
no | no | no | yes, while enabled on the server |
capabilities (raw) |
no (session.capabilities is a separate source, below) |
no | yes, whole | yes |
Each setting cascades as config < client < workspace < call, through resolve
in workspace/settings.py. version and capabilities join SETTINGS:
-
version: (version,x-version, a stripped non-empty string).latestis kept as a value, so an explicit clear beats a remembered version. -
capabilities: (None,None, a dict coercion).{}is kept as a value, so an explicit clear beats a remembered object, the wayinsecure=falsebeats a rememberedtrue.
Both are stored in Workspace.settings, so Workspaces.resolved() replays
them after a reap with no new path. The browser switch that drops a remembered
version is applied in open_browser, before resolve, to the inherited
settings.
The config's session.capabilities is not a lower layer of the raw
setting. It is its own source, merged under the raw object at every open
(ruling 14). That way a config change reaches the next open even in a
workspace that remembers a raw object.
This is the one place the precedence rules live. It is a pure function with
no selenium import, in core/session_caps.py, because core/capabilities.py
is already the table of actions.
It takes:
- the browser kind;
- the resolved named options, and
option_args(ruling 29); - the config capabilities;
- the raw object.
It returns:
- the dict to send;
- for each top-level key, its value and its sources;
-
ignored: the vendor entries where ours beat the caller's (ruling 28); - or a
ValueErrorfor an owned key or a vendor block of the wrong shape, which is a 400.
Grid._options turns that dict into the Options object it sends, whose
to_capabilities() returns exactly that dict (ruling 23). Grid.reconnect()
calls _options() with no arguments for an inert ReattachDriver, and that
call must stay cheap and valid.
The layers are applied in this order:
-
flow: what selenium-flow always sends.
-
unhandledPromptBehavior: ignore,se:downloadsEnabled: trueandwebSocketUrl: true. - The vendor block's args and prefs from today's
_options. Chrome gets--no-sandbox,--disable-dev-shm-usageand four prefs; Firefox gets five prefs.
-
-
option: what a named option produces.
-
browserNamefrombrowser, always. -
browserVersionfromversion, unless it is unset orlatest. -
acceptInsecureCertsfrominsecure, always,trueorfalse(ruling 23). -
se:recordVideoandse:videoNamefromrecord. - A vendor arg from
headless, which #86 adds throughoption_args.
-
-
config:
session.capabilities. -
raw: the caller's
capabilities.
| Source | May override | Refused when it sets | Silently loses to |
|---|---|---|---|
| flow | nothing | nothing | nothing |
| option | nothing; it sets only the keys it owns | nothing | nothing (its vendor arg is added beside flow's) |
| config | nothing of flow's or option's | an owned key: a ConfigError at load, naming the option |
raw, for the same key |
| raw | a config key, whose source becomes raw
|
an owned key: a 400 naming the option | flow and option, inside a vendor block (ruling 5); listed in ignored
|
Owned keys, and what a refusal names:
| Key | The refusal says |
|---|---|
unhandledPromptBehavior |
selenium-flow keeps dialogs open for the dialog tool; it cannot be set |
se:downloadsEnabled |
the downloads folder needs it; it cannot be set |
webSocketUrl |
site data, capture and the monitor need it; it cannot be set |
browserName |
use browser=
|
browserVersion |
use version=
|
acceptInsecureCerts |
use insecure= (true or false) |
se:recordVideo, se:videoName
|
use record=true
|
timeouts |
use page_load_timeout= / script_timeout=
|
| the headless flag in a vendor block's args | use headless=true (#86 adds this row) |
A refusal is raised before the held browser is ended, like today's strict
browser check in open_browser: a rejected argument must cost nothing.
The vendor block merge applies to goog:chromeOptions for Chrome and
moz:firefoxOptions for Firefox.
-
argsare listed ours first, then option-produced, then config, then raw, with exact duplicates removed. -
prefsmerge as a dict. Flow beats everything, option beats config and raw, and raw beats config. -
Any other key in the block (
binary,extensions,log,mobileEmulation, …) comes from config, then raw (raw wins), unless flow sets it. - The block's sources are every layer that contributed to it.
-
A block for the other browser (a
moz:block on Chrome) is sent untouched, best effort.
The function is where the unit tests live: every row of both tables gets one.
Workspace (workspace/store.py) gains three fields. Each gets a defensive
reader in from_json, like _timeout and _size, so an old or malformed
record reads as none.
-
sent:list[{key, value, sources[]}], what this session's open sent.remember()writes it at every open and every reopen. -
ran:{version, node, live_view}, read fromdriver.capsat open (browserVersion,se:containerName,se:vncEnabled). -
ended:{at, by}or None, wherebyisadminorcaller.end_browserwrites it fromcaller.named_by == "admin"; the admin End already passesCaller(key, "admin"). The next open clears it.
detached() keeps sent and ran, because the idle GRID SESSION card shows
the last browser. A reap writes nothing here: nothing watches a plain session,
so the admin view learns a browser is gone from the record still naming a
browser the Grid no longer lists.
-
open_session's result.-
settingsgainsversionas asked, andcapabilities(the raw object) when there is one. - The top level gains
versionas it ran (ran.version). - The top level also gains
ignored, the vendor entries ours beat, but only when it is not empty. -
CAPABILITIES["open_session"].responseincore/capabilities.pyis updated with these fields.
-
-
workspace://current(Workspaces.describe) gainsversionas it ran, besidebrowser.settingsalready shows what is remembered.
A model is never shown the full sent set. It belongs to the admin, and it is too long to be worth a model's context.
All of this goes in errors.py, the one place that decides what a failure
means.
-
An owned key, or a vendor block of the wrong shape, is a 400: a
ValueErrorfrom the builder. -
The queue timed out: a
SessionNotCreatedExceptionwhose message containsNew session request timed outorTimed out creating session. It is a 503, with our own text, for example:no Grid node took this browser before the Grid gave up waiting: it asked for chrome 152, kubed:pool=headless. The Grid may have no node with these capabilities, or none free; see skill://selenium-flow/references/TROUBLESHOOTING.md
The message quotes what ruling 25 allows, and never a vendor block. Both Grid strings live in one constant, and a test pins them.
-
A Grid-side failure (ruling 24) stays the 503 it is today, with the Grid's own words.
-
A node refused the session: any other
SessionNotCreatedException. Its status depends on what the open sent (ruling 16):-
400 when the open sent raw keys (from
capabilities) or aversion. The message quotes the node's reason and names those keys or that version as the usual cause. - 503 when it sent neither. The message quotes the node's reason and says the node could not start the browser, so trying again may succeed.
It is a new fault type, raised from the open. It carries whether the caller sent raw keys or a version, so
errors.pycan choose the status. Config capabilities do not count, because they are the operator's. -
400 when the open sent raw keys (from
-
The published contract.
POST /browseralready publishes 400 and 503; their descriptions inspec/name the new causes.
SessionSettings (config.py) gains two settings:
-
capabilities:Annotated[dict[str, JsonValue], NoDecode], default{}. The env varSESSION_CAPABILITIESis JSON. An owned key is aConfigErrorat load, naming the option. -
version:str | None.
FROM_CONFIG in workspace/settings.py gains version. The Settings tab
shows both, since it renders every leaf. tests/test_config_load.py covers
the JSON from the environment and the refusal of an owned key.
The per-workspace payload in http/admin/workspaces.py gains sent, ran,
ended and asked_version (ruling 30). It already carries session_id,
started and grid_timeout.
The UI is design/session.md, in four parts:
- The masthead. The summary card shrinks to the browser mark, the name, the last page and the existing pills, moved as they are. #83 renames the pills.
- The Session tab comes first, and a workspace opens on it (ruling 31).
-
The two cards:
-
WORKSPACE has rows BROWSER as asked (
firefox · 142,chrome · latest), WINDOW, RECORD (only when on) and CAPTURE (only while a browser is open). -
GRID SESSION has rows BROWSER as it ran, ID, NODE, STARTED, IDLE
TIMEOUT and LIVE VIEW (
yes/no). On an idle workspace, ENDED takes LIVE VIEW's place. The card is not shown at all if no browser was ever opened.
-
WORKSPACE has rows BROWSER as asked (
-
Capabilities is a section, collapsed by default.
- Each row is a key, a value and a source pill:
flowgrey outline,configblue outline,optionsolid blue,rawsolid ink. - A vendor block is summarised as
args N · prefs N, with one pill for each source that fed it.
- Each row is a key, a value and a source pill:
The ENDED wording (agent's call):
| How it ended | ENDED reads |
|---|---|
| the admin's End | <local time> · ended by the admin |
the caller's end_browser
|
<local time> · ended by the caller |
| the record still names a browser the Grid no longer lists |
reaped after <grid_timeout> s idle, with no time, because nothing watched it |
| the same, with no idle timeout known | gone from the Grid |
Agent's call:
-
references/WORKSPACES.mdgains Choosing the browser: the named options as one set, what is remembered, andversionwithlatest. -
references/TROUBLESHOOTING.mdgains three entries:- the raw
capabilitiesescape hatch, as a last resort, with owned keys refused; - no node took this browser: the 503. Check the capabilities you sent, and look at the Grid console;
- a node refused: the 400. A raw key is the usual cause.
- the raw
-
references/CONFIGURATION.mdnamesSESSION_VERSION,SESSION_CAPABILITIESand?version=/X-Version. -
SKILL.mdgets one routing row.
It is written for a model deciding what to do next.
- The pools catalogue (ruling 2).
- CDP.
- Reading KEDA or Kubernetes from selenium-flow.
- The live view (#83).
- Headless (#86).
- A per-session browser log.
- Giving up on an open before the Grid does (ruling 12).
Unit tests:
- every row of the builder's two tables, and rulings 23, 27, 28 and 29;
-
settings.resolveforversionandcapabilities: the clear markers, and a browser switch dropping a remembered version; -
from_jsonon the three new fields; - the error mapping, with the two queue strings and the Grid-side strings
pinned, and a node refusal answered 400 with raw keys or a
versionsent and 503 with neither; - config load:
SESSION_CAPABILITIESas JSON, and an owned key refused; - the
open_sessionresponse shape.
These must pass unchanged in intent: tests/test_surfaces.py,
test_request_schemas_are_the_tool_schemas, and tests/test_wiki.py once the
pages are regenerated with scripts/generate_wiki.py.
No new integration flow (AGENTS.md: less is more). One existing flow changes its URL (ruling 31).
The checks:
-
ruff check .andpytest; -
npm --prefix ui ci && npm --prefix ui run build, thentest,checkandlint.
One CHANGELOG line, for example: "open_session takes version and raw
capabilities, and the admin UI's new Session tab shows what was sent and
what the Grid gave." Ruling 23 adds a Fixed: line: "Firefox accepts a
self-signed certificate only with insecure=true, like Chrome."
Nothing is open. The last question, whether Firefox keeps accepting every
certificate, was answered by Dr K on PR #87 (ruling 23): fix it, with
insecure always sending acceptInsecureCerts.
core/
-
browser.py:Grid._options(lines 215–308),Grid.open(310),Grid.reconnect(323, calls_options()inert),Grid.sessions(422, reads version, node and vnc from/status). -
actions.py:open_session(220), and_set_up(308), whoseapplied(395) becomessettings. -
capabilities.py: theopen_sessionrow (208) and itsresponse.
mcp/
-
tools.py:open_session(357), an@owndeclaration whose docstring is prompt.
workspace/
-
settings.py:SETTINGS(82),FROM_CONFIG(101),from_client,resolve(134). -
workspaces.py:-
CLIENT_PARAMS/CLIENT_HEADERS(132), derived fromSETTINGS; -
describe(339); -
resolved(485), which replays the settings after a reap through**replay, soactions.open_sessionmust take every stored setting as a keyword; -
open_browser(816), which makes its strict checks beforeend_browser(865) and does not inherit record or capture; -
remember(957); -
end_browser(1110), whosedetached()is at 1155.
-
-
store.py:Workspace(97) andfrom_json(168).
http/admin/
-
workspaces.py: the per-workspace payload (501–560), and the admin End (662), which callsend_browserwithCaller(key, "admin").
Elsewhere
-
routes.py:opened(243) passes everySETTINGSkey of the body, so the endpoint takesversionandcapabilitiesonce they are settings. -
server.py: buildsGrid,ActionsandWorkspaces(86–206). -
errors.py:UNAVAILABLE, which holdsSessionNotCreatedException, andstatus_for. -
faults.py: the package's own exception types. -
flows/document.py:NOT_STEPSlistsopen_session, so flows are untouched. -
ui/src/lib/WorkspaceSummary.svelte(the masthead),ui/src/admin/WorkspaceDetail.svelte(the tabs, 311–345),ui/src/admin/router.svelte.ts(Tab,TABS,hashes),ui/src/admin/Section.svelte(opens by default), and the source pill styles scoped inui/src/admin/SettingsPane.svelte(68–71). - The monitor watches only the sessions it owes something (capture or a
recording). A plain reap is learned at the next
resolve, or from the admin list's/statusjoin.
The action pages are generated from openapi.yaml, which is itself generated from the live MCP tool schemas — so they describe the server that shipped, not the one someone remembered. Prose belongs in wiki-notes/<tool>.md in the repo.
selenium-flow · MIT
Start here
Guides
Lifecycle
Going places
Doing things
Getting things out
Site data
Console and network