Skip to content

2026 10 28 headless

kubed-io[bot] edited this page Oct 28, 2026 · 1 revision

Headless: a remembered setting that rules out recording and the live view: Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: open_session(headless=true) opens a browser that draws nothing on the node's screen. The workspace remembers it as it remembers insecure. record=true on it is a 400 saying where headless came from, its live view is refused, and a headless flag smuggled through raw capabilities is refused in the call (400) and in config (boot).

Architecture: headless is one more SETTINGS row, so the cascade, the record and the reap carry it with no new code. Actions.open_session turns it into session_caps.build(option_args=…), which #82 left for this. session_caps.check gains the guard, so every place that already checks (the call, the reap, the config at load) refuses the flag. open_browser refuses record beside RECORDING_OFF. The live proxy's look() and the UI's liveUnavailable read settings.headless off the record.

Tech Stack: Python 3.11+, Selenium 4.48+, FastMCP 4, pytest; Svelte 5 runes and Vitest with jsdom in ui/.

Spec: docs/superpowers/specs/2026-10-28-headless-design.md, drafted on #86 and finished here rather than replaced. Rulings 1–10 are settled. This round changed three of the agent's calls, none of Dr K's: the guard covers both vendor blocks (ruling 6), the admin row gains headless, and "the headless default" leaves the prose (ruling 10).

Design

The spec is the design. What this plan settles where the spec left the call to it:

  • Names in core/session_caps.py.
    • HEADLESS_ARGS = {CHROME: "--headless=new", FIREFOX: "-headless"}.
    • HEADLESS_OWNED = "use headless=true or headless=false instead".
    • _headless(arg: str) -> bool, which is arg.lstrip("-").split("=", 1)[0].lower() == "headless".
  • The guard's order inside check. Owned top-level keys are refused first, as today. Then, per vendor block, the shape checks. Then every headless flag across both blocks is refused in one ValueError, joined by "; ", each f"{block}.args {arg} cannot be sent in {where}: {HEADLESS_OWNED}" or f"moz:firefoxOptions.env.MOZ_HEADLESS cannot be sent in {where}: {HEADLESS_OWNED}", plus REMEMBERED when remembered. env is looked at only when it is a mapping; its shape is geckodriver's to refuse.
  • Names in workspace/workspaces.py, beside RECORDING_OFF:
    • HEADLESS_RECORD = "a headless browser draws nothing to film: record=true cannot be asked with headless=true";
    • HEADLESS_RECORD_REMEMBERED = "this workspace's browser is headless, remembered from an earlier open: pass headless=false to record".
  • Actions.open_session(…, capabilities=None, headless=None, *, …). It goes after capabilities, so no positional caller shifts. _set_up gains headless: bool, and applied["headless"] = True follows insecure.
  • The tool's parameter headless: bool | None = None goes after insecure, the option it is remembered like. Adding an optional argument is safe (CONTRIBUTING, Changing an argument's shape).
  • The admin row gains "headless": bool(settings.get("headless")) after insecure. WorkspaceRow gains headless?: boolean. The WORKSPACE card's row is ['headless', row.headless ? 'on' : null, sources('headless')], after INSECURE.

Global Constraints

  • Python 3.11 is the floor: nothing 3.12+.
  • core/session_caps.py stays plain: no Selenium, no protocol import (tests/test_boundaries.py).
  • No query parameter, header or config setting for headless (ruling 2). FROM_CONFIG does not change.
  • No log line formats a Grid id, a capability value or a setting's value.
  • ran.live_view stays se:vncEnabled (ruling 5).
  • Every published shape that moves is a golden: regenerate with GOLDEN_UPDATE=1 pytest tests/test_golden.py, read the diff, and commit goldens on their own (CONTRIBUTING.md).
  • Wiki pages are regenerated with python scripts/generate_wiki.py, and tests/test_wiki.py passes. The wiki is the docs/ submodule: a commit inside it, then the pointer here.
  • Changelog: only ## [Unreleased], one short line.
  • No new integration flow (AGENTS.md: less is more).
  • The UI copy rules: no helper text in a value.
  • Never commit kubed/selenium_flow/http/static/ or openapi.yaml.
  • The checks, as CI runs them:
    • Python (test.yml): ruff check . and pytest.
    • UI (ui.yml): npm --prefix ui ci, npm --prefix ui run lint, npm --prefix ui run check, npm --prefix ui test, npm --prefix ui run build.
    • OpenAPI (quality.yml): python scripts/generate_openapi.py succeeds.

Review Focus

  1. A workspace that remembers headless, and a caller who now asks open_session(record=true). A person expects a 400 that says the headless came from an earlier open and how to clear it, and the browser they hold still running. Task 4 tests the wording and actions.calls/grid.alive untouched.
  2. The same caller then passes open_session(record=true, headless=false). A person expects a recorded, headful browser, and the next open_session() to stay headful. Task 4 tests both opens.
  3. A Chrome workspace whose remembered capabilities hold {"moz:firefoxOptions": {"args": ["-headless"]}}, from before the upgrade, then open_session(browser="firefox"). A person expects a refusal saying to clear it with capabilities={}, not a headless Firefox the rules never saw. Task 2 tests the block for the other browser, and Task 4 tests the explicit open and the reap.
  4. An HTTP caller sending {"headless": "false"} as a string, or ?headless=true on its URL. A person expects the string read as false and beating a remembered true, and the URL ignored. Task 4 tests both through open_browser.
  5. The admin opening the Live tab of a headless browser on a node that reports VNC. A person expects the tab greyed no live view · headless, and a forced mint refused, not an empty desktop. Task 5 tests the mint and the socket, and Task 6 the tab's order.

Task 1: Measure what the spec could not

Files:

  • Modify: docs/superpowers/specs/2026-10-28-headless-design.md, Research (the two measured bullets)

Interfaces: none. Nothing in the repository changes but the spec.

  • Step 1: A node with no screen (ruling 9). For each image, docker run -d --rm -p 4444:4444 --shm-size 2g -e SE_START_XVFB=false selenium/standalone-chrome:4.48.0-20260905 (then selenium/standalone-firefox:4.48.0-20260905), wait for /opt/bin/check-grid.sh, and open a headful browser with a scratch script outside the repository:
from selenium import webdriver
from selenium.common.exceptions import SessionNotCreatedException
try:
    webdriver.Remote("http://localhost:4444", options=webdriver.ChromeOptions())  # FirefoxOptions
except SessionNotCreatedException as e:
    print(e.msg)

Record the message with its hosts and build lines cut, as session_caps._reason would cut them. If it opens instead, record that.

  • Step 2: The window cap (ruling 8). On each image with its default screen, open headful and then with --headless=new / -headless, call set_window_size(3000, 4000), and record what get_window_size() returns.

  • Step 3: Write both into Research, replacing the "What the plan's Task 1 measures" paragraph with the results and the date. If Docker is not available where this runs, write not measured: <why> for each, keep ruling 8's sentence as the issue's research states it, and say so in the pull request. If Step 2 finds the headful window not capped, stop and tell Dr K: ruling 8's docstring sentences in Task 3 would be false.

  • Step 4: Commit inside docs/: docs: what a headful browser gets with no screen, and the window cap.

No check applies: nothing outside the spec changes.


Task 2: The guard, and the flag sent

Files:

  • Modify: kubed/selenium_flow/core/session_caps.py: new names after OWNED (53–67); check (175–207); build's docstring (276–278, "nothing passes it yet")
  • Test: tests/test_session_caps.py, tests/test_config_load.py

Interfaces:

  • Produces: HEADLESS_ARGS, HEADLESS_OWNED, _headless (Design). check(raw, *, where, remembered) keeps its signature. build is unchanged: option_args already exists.

  • Step 1: Write the failing tests. In tests/test_session_caps.py:

@pytest.mark.parametrize("block", ["goog:chromeOptions", "moz:firefoxOptions"])
@pytest.mark.parametrize("arg", ["--headless", "--headless=new", "--headless=old", "-headless", "-HEADLESS", "headless"])
@pytest.mark.parametrize("browser", ["chrome", "firefox"])
def test_a_headless_arg_is_refused_in_either_block_on_either_browser(block, arg, browser):  # ruling 6
    said = f"{block}.args {arg} cannot be sent in capabilities: use headless=true or headless=false instead"
    with pytest.raises(ValueError, match=re.escape(said)):
        build(browser, raw={block: {"args": ["--lang=fr", arg]}})

def test_moz_headless_in_env_is_refused_whatever_its_value():
    for value in ("1", "0", ""):
        with pytest.raises(ValueError, match=re.escape(
                "moz:firefoxOptions.env.MOZ_HEADLESS cannot be sent in capabilities: use headless=true")):
            check({"moz:firefoxOptions": {"env": {"MOZ_HEADLESS": value}}})

def test_args_that_only_look_like_headless_pass():
    check({"goog:chromeOptions": {"args": ["--headless-mode-x", "--no-headless-thing", "--lang=headless"]}})

def test_a_headless_flag_in_config_names_session_capabilities():
    with pytest.raises(ValueError, match=re.escape(
            "goog:chromeOptions.args --headless cannot be sent in session.capabilities: use headless=")):
        build("chrome", config={"goog:chromeOptions": {"args": ["--headless"]}})

def test_a_remembered_headless_flag_says_how_to_clear_it():
    with pytest.raises(ValueError, match=re.escape("open_session(capabilities={})")):
        check({"moz:firefoxOptions": {"args": ["-headless"]}}, remembered=True)

def test_two_headless_flags_are_named_in_one_refusal():
    with pytest.raises(ValueError) as refused:
        check({"goog:chromeOptions": {"args": ["--headless"]}, "moz:firefoxOptions": {"env": {"MOZ_HEADLESS": "1"}}})
    assert "goog:chromeOptions.args --headless" in str(refused.value)
    assert "moz:firefoxOptions.env.MOZ_HEADLESS" in str(refused.value)

def test_the_headless_args_are_each_browsers_own():  # ruling 7
    assert HEADLESS_ARGS == {"chrome": "--headless=new", "firefox": "-headless"}

test_an_option_arg_joins_the_browsers_own_block (126) already pins the arg's place and its option source; leave it. In tests/test_config_load.py, beside test_an_owned_key_in_session_capabilities_stops_the_boot:

def test_a_headless_flag_in_session_capabilities_stops_the_boot():  # ruling 6
    with pytest.raises(ConfigError) as refused:
        load([], {"SESSION_CAPABILITIES": '{"moz:firefoxOptions": {"args": ["-headless"]}}'})
    assert "moz:firefoxOptions.args -headless cannot be sent in session.capabilities: " \
           "use headless=true or headless=false instead" in str(refused.value)
  • Step 2: Run them to see them fail

Run: pytest tests/test_session_caps.py tests/test_config_load.py -v Expected: FAIL, HEADLESS_ARGS not defined, and the refusals not raised.

  • Step 3: Implement the names and the guard as Design orders them. build's docstring says option_args is what headless adds.

  • Step 4: Run them

Run: pytest tests/test_session_caps.py tests/test_config_load.py tests/test_boundaries.py -v && ruff check . Expected: PASS.

  • Step 5: Commit feat: a headless flag in raw or config capabilities is refused, naming headless=.

Task 3: The option, on the action and both surfaces

Files:

  • Modify: kubed/selenium_flow/workspace/settings.py: SETTINGS (130–152), a headless row after insecure; the module docstring's list of settings (3–6)
  • Modify: kubed/selenium_flow/core/actions.py: open_session (273–387), _set_up (389–518)
  • Modify: kubed/selenium_flow/mcp/tools.py: open_session's parameter and docstring (377–448), resize's docstring (602), screenshot's docstring (778–)
  • Test: tests/test_session_open.py, tests/test_surfaces.py
  • Regenerate: tests/golden/tools-on.json, tests/golden/tools-off.json, tests/golden/openapi.json; wiki pages

Interfaces:

  • Consumes: HEADLESS_ARGS (Task 2).

  • Produces: SETTINGS["headless"] == (None, None, _as_flag); Actions.open_session(…, headless=None); the result's settings["headless"] is True when set, absent otherwise.

  • Step 1: Write the failing tests. In tests/test_session_open.py, with its Grid and Driver:

@pytest.mark.parametrize("browser, arg, block", [
    ("chrome", "--headless=new", "goog:chromeOptions"), ("firefox", "-headless", "moz:firefoxOptions")])
def test_headless_sends_the_browsers_own_flag_and_says_so(browser, arg, block):  # ruling 7
    grid = Grid(RAN)
    result = Actions(grid).open_session(browser=browser, headless=True)
    assert arg in grid.built.send[block]["args"]
    assert result["settings"]["headless"] is True

@pytest.mark.parametrize("given", [False, None, "false"])
def test_headful_sends_no_flag(given):
    grid = Grid(RAN)
    result = Actions(grid).open_session(headless=given)
    assert not any("headless" in a for a in grid.built.send["goog:chromeOptions"]["args"])
    assert "headless" not in result["settings"]

def test_headless_over_http_reaches_the_open(...):   # POST /browser {"headless": true} on a SeleniumMCP
    # whose actions.grid is this file's Grid: the endpoint passes it because it is a SETTINGS key
    assert "--headless=new" in grid.built.send["goog:chromeOptions"]["args"]

In tests/test_surfaces.py, beside the existing schema checks: open_session's input schema has headless as an optional boolean (anyOf boolean / null), and it is not required.

  • Step 2: Run them to see them fail

Run: pytest tests/test_session_open.py tests/test_surfaces.py -v Expected: FAIL, open_session takes no headless.

  • Step 3: Implement the row, the action's keyword and _set_up's report, and the tool's parameter, passed through to workspaces.open_browser. Then the prose:

    • open_session's docstring. In the window bullet, "the headless default is narrow" becomes "the default window is narrow" (ruling 10). A new bullet goes after the insecure one and before record:

      headless=true opens a browser that draws nothing on the node's screen: quicker, and its window is not limited to that screen, so it is the way to a very large window or full-page screenshot. It cannot be recorded and nobody can watch it live, and sites' bot checks spot it more easily, so leave it off for real sites. Remembered like the options above until you pass headless=false.

    • resize's docstring: "the headless default is small" becomes "the default window is small".

    • screenshot's docstring, one sentence after the full_page one: full_page grows the window to the page, and a window cannot outgrow the node's screen unless the browser was opened with headless=true. Drop it if Task 1 measured no cap.

  • Step 4: Run them, regenerate, and read the diffs

Run: pytest tests/test_session_open.py tests/test_surfaces.py tests/test_browser_choice.py -v && GOLDEN_UPDATE=1 pytest tests/test_golden.py && python scripts/generate_wiki.py && pytest && ruff check . && python scripts/generate_openapi.py Expected: PASS. The golden diff shows headless in open_session's schema and the three descriptions, in tools-on, tools-off and openapi, and nothing else.

  • Step 5: Commit the code, then the goldens, then the wiki pages: feat: open_session(headless=true).

Task 4: Remembered, and no recording

Files:

  • Modify: kubed/selenium_flow/workspace/workspaces.py: the two refusals beside RECORDING_OFF (78–83); open_browser (1044–1047)
  • Test: tests/test_workspaces.py (beside test_version_and_capabilities_are_remembered_and_carried, 1818), tests/test_recording_open.py

Interfaces:

  • Consumes: SETTINGS["headless"] (Task 3); the guard (Task 2).
  • Produces: HEADLESS_RECORD, HEADLESS_RECORD_REMEMBERED.

In open_browser, right after the RECORDING_OFF check:

if settings.get("record") and settings.get("headless"):
    passed = settings_module.SETTINGS["headless"][2](wanted.get("headless")) is not None
    raise ValueError(HEADLESS_RECORD if passed else HEADLESS_RECORD_REMEMBERED)

The cascade itself needs no code: the tests prove it carries the new row.

  • Step 1: Write the failing tests. In tests/test_workspaces.py, with manager and RecordingActions:
def test_headless_is_remembered_and_carried():                         # ruling 2
    workspaces.open_browser(Caller(NAMED), headless=True)
    workspaces.open_browser(Caller(NAMED))
    assert actions.opened_settings[-1]["headless"] is True
    assert workspaces.store.get(NAMED).set_by["headless"] == "caller"

def test_an_explicit_false_beats_a_remembered_true_and_is_remembered():  # Review Focus 2, 4
    workspaces.open_browser(Caller(NAMED), headless=True)
    workspaces.open_browser(Caller(NAMED), headless="false")
    workspaces.open_browser(Caller(NAMED))
    assert actions.opened_settings[-1]["headless"] is False

def test_a_reap_replays_headless():
    workspaces.store.set(NAMED, Workspace(session_id="dead",
        settings={"browser": "chrome", "headless": True}, set_by={"browser": "caller", "headless": "caller"}))
    workspaces.resolve(NAMED)
    assert actions.opened_settings[-1]["headless"] is True

def test_headless_is_read_from_no_parameter_and_no_header():           # Review Focus 4
    caller = Caller.from_request({"workspace": [NAMED], "headless": ["true"]}, {"x-headless": ["true"]})
    workspaces.open_browser(caller)
    assert "headless" not in actions.opened_settings[-1]

def test_a_remembered_raw_headless_flag_is_refused_on_an_explicit_open():  # Review Focus 3
    # stored, attached and alive: settings {"capabilities": {"moz:firefoxOptions": {"args": ["-headless"]}}}, set_by caller
    with pytest.raises(ValueError, match=re.escape("open_session(capabilities={})")):
        workspaces.open_browser(Caller(NAMED), browser="firefox")
    assert actions.closed == []

def test_a_reap_refuses_a_remembered_raw_headless_flag():
    # the same settings on a dead session: resolve raises with "open_session(capabilities={})", actions.opened == 0

In tests/test_recording_open.py, with its manager(Recorder()):

def test_record_with_headless_in_the_same_call_is_a_400_and_opens_nothing():  # ruling 4
    m = manager(Recorder())
    with pytest.raises(ValueError) as exc:
        m.open_browser(caller(), record=True, headless=True)
    assert str(exc.value) == workspaces_module.HEADLESS_RECORD
    assert m.actions.calls == []

def test_record_on_a_remembered_headless_says_where_it_came_from_and_keeps_the_browser():  # Review Focus 1
    m = manager(Recorder())
    m.open_browser(caller(), headless=True)
    with pytest.raises(ValueError) as exc:
        m.open_browser(caller(), record=True)
    assert str(exc.value) == workspaces_module.HEADLESS_RECORD_REMEMBERED
    assert m.actions.grid.alive == {"grid0001"} and len(m.actions.calls) == 1

def test_headless_false_lets_a_remembered_headless_workspace_record():   # Review Focus 2
    m = manager(Recorder())
    m.open_browser(caller(), headless=True)
    assert m.open_browser(caller(), record=True, headless=False)["recording"] is True
    assert m.actions.calls[-1]["headless"] is False

def test_recording_off_is_said_before_headless():
    with pytest.raises(ValueError) as exc:
        manager(None).open_browser(caller(), record=True, headless=True)
    assert str(exc.value) == workspaces_module.RECORDING_OFF
  • Step 2: Run them to see them fail

Run: pytest tests/test_workspaces.py tests/test_recording_open.py -v Expected: the record tests FAIL, HEADLESS_RECORD not defined. The cascade tests may already pass after Task 3; that is the point of them.

  • Step 3: Implement the two constants and the check.

  • Step 4: Run the suite

Run: pytest && ruff check . Expected: PASS.

  • Step 5: Commit feat: a headless workspace refuses record=true, saying where headless came from.

Task 5: No live view, and the admin row

Files:

  • Modify: kubed/selenium_flow/http/admin/live.py: HEADLESS beside NO_LIVE_VIEW (55); look (140–149)
  • Modify: kubed/selenium_flow/http/admin/workspaces.py: the row (522)
  • Test: tests/test_admin_live.py (build, 47–59; the two parametrised refusals, 103–115 and 200–218), tests/test_files_and_admin.py
  • Regenerate: tests/golden/admin-workspaces.json

Interfaces:

  • Consumes: the stored settings["headless"] (Tasks 3, 4).
  • Produces: live.HEADLESS = "this browser is headless: it has no live view"; the row's headless: bool.

look, after the NO_BROWSER return: if (record.settings or {}).get("headless"): return None, HEADLESS.

  • Step 1: Write the failing tests. In tests/test_admin_live.py, build stores one more record:
store.set("headless", Workspace(session_id="g-headless", ran={"live_view": True},
                                settings={"headless": True}, set_by={"headless": "caller"}))

and both parametrised lists gain ("headless", True, "this browser is headless: it has no live view"), so the mint answers 404 and the socket closes 4004 with it, with the audit line. In tests/test_files_and_admin.py, beside test_the_row_carries_insecure_and_each_settings_source (805) and built the same way:

def test_the_row_says_whether_the_browser_is_headless():
    # one record with settings {"headless": True}, set_by caller; one with none
    assert [row["headless"] for row in rows] == [True, False]
  • Step 2: Run them to see them fail

Run: pytest tests/test_admin_live.py tests/test_files_and_admin.py -v Expected: FAIL: the headless record mints a link, and the row has no headless.

  • Step 3: Implement HEADLESS, the check, and the field.

  • Step 4: Run them and regenerate the admin golden

Run: pytest tests/test_admin_live.py tests/test_files_and_admin.py -v && GOLDEN_UPDATE=1 pytest tests/test_golden.py && pytest && ruff check . Expected: PASS. The golden diff adds headless to each row and nothing else.

  • Step 5: Commit the code, then the golden: feat: a headless browser has no live view, and the admin row says so.

Task 6: The Session and Live tabs

Files:

  • Modify: ui/src/lib/types.ts (WorkspaceRow, 46), ui/src/admin/liveview.svelte.ts (liveUnavailable, 24–30), ui/src/admin/SessionPane.svelte (27)
  • Test: ui/src/admin/liveview.test.ts (236–253), ui/src/admin/SessionPane.test.ts (beside the INSECURE test, 150–159)

Interfaces:

  • Consumes: the row's headless (Task 5).

  • Produces: WorkspaceRow.headless?: boolean; liveUnavailable returns 'no live view · headless' after no browser, before this Grid has no VNC.

  • Step 1: Write the failing tests. In liveview.test.ts:

test('a headless browser has no live view, said after no browser and before the Grid', () => {
  const headless = { key: 'k', live: true, headless: true, ran: { version: null, node: null, live_view: true } }
  expect(liveUnavailable(headless, true)).toBe('no live view · headless')
  expect(liveUnavailable({ ...headless, ran: { version: null, node: null, live_view: false } }, true)).toBe('no live view · headless')
  expect(liveUnavailable({ ...headless, live: false }, true)).toBe('no live view · no browser')
  expect(liveUnavailable(headless, false)).toBe('no live view · switched off')
})

In SessionPane.test.ts:

test('HEADLESS shows only when on, idle too, with its source', () => {
  const on = render(SessionPane, { row: { ...LIVE, live: false, attached: false, headless: true, set_by: { headless: 'caller' } }, hidden: false })
  expect(fact(on.container, 'workspaceCard', 'headless')).toBe('on')
  expect(pillsOf(on.container, 'headless').map((p) => p.textContent)).toEqual(['caller'])
  const off = render(SessionPane, { row: { ...LIVE, headless: false }, hidden: false })
  expect(fact(off.container, 'workspaceCard', 'headless')).toBeUndefined()
})
  • Step 2: Run them to see them fail

Run: npm --prefix ui ci && npm --prefix ui test Expected: FAIL.

  • Step 3: Implement the type, the branch and the row.

  • Step 4: Run every UI check

Run: npm --prefix ui run lint && npm --prefix ui run check && npm --prefix ui test && npm --prefix ui run build Expected: PASS. Then look at it: npm --prefix ui run dev against docker compose up, open a workspace with headless=true, and compare the Session and Live tabs with the Penpot pages Workspace · Session and Workspace · Live (live-unavailable). Say so if that could not be done.

  • Step 5: Commit feat: the Session tab shows HEADLESS, and the Live tab says why it is greyed.

Task 7: What a model, an operator and a maintainer are told

Files:

  • Modify: skills/selenium-flow/references/WORKSPACES.md, Choosing the browser (76–109)
  • Modify: skills/selenium-flow/references/TROUBLESHOOTING.md, Sending a capability no option covers (207–231)
  • Modify: skills/selenium-flow/references/CONFIGURATION.md (142–145)
  • Modify: skills/selenium-flow/references/READING_PAGES.md (153–154), skills/selenium-flow/references/INTERACTION.md (249–250)
  • Modify: docs/notes/open_session.notes.md (37–58)
  • Modify: AGENTS.md: The live view (the bullet list after "A person in Control holds the browser") and Recordings
  • Modify: CHANGELOG.md (## [Unreleased])
  • Regenerate: wiki pages
  • Test: tests/test_skill.py, tests/test_wiki.py

Interfaces: none produced.

Content:

  • WORKSPACES.md. A table row after insecure: | `headless` | draws nothing on the node's screen: quicker, and the window is not limited to it; no recording, no live view | keeps it if you passed it |. Under the paragraph after the table, a short one: choose it for speed, a very large window or full-page screenshot, or when nobody will watch; leave it off for real sites, whose bot checks spot it more easily; record=true on a headless browser is refused, so pass headless=false to record.

  • TROUBLESHOOTING.md. The owned list gains a headless flag in either browser's args, or MOZ_HEADLESS in Firefox's env (headless=). The node-class example {"kubed:pool": "headless"} becomes {"kubed:pool": "large"}, here and in docs/notes/open_session.notes.md, since a pool called headless now reads as the option.

  • CONFIGURATION.md, after the insecure paragraph: headless has no default either: only open_session(headless=true) sets it, and a headless flag in session.capabilities stops the boot.

  • READING_PAGES.md, INTERACTION.md: "headless defaults" / "The headless default" become "the default window" (ruling 10).

  • open_session.notes.md: the owned-key list gains the headless flag.

  • AGENTS.md, one bullet in The live view: A headless browser has no live view: look() refuses it on the record's settings.headless, never on ran.live_view, which stays what the Grid said (a headless browser on a node with a screen still reports VNC). One in Recordings: record=true with headless is a 400 before anything ends, in words that say whether headless was passed or remembered; a headless flag in raw or config capabilities is refused, so the named option is the only way to it.

  • CHANGELOG, one line: - **`open_session(headless=true)`** opens a browser that draws nothing on screen, remembered until `headless=false`; it cannot be recorded or watched live.

  • Step 1: Write the content.

  • Step 2: Regenerate and run the published-shape tests

Run: python scripts/generate_wiki.py && pytest tests/test_skill.py tests/test_wiki.py tests/test_readme.py -v Expected: PASS.

  • Step 3: Run every check once more

Run: ruff check . && pytest && npm --prefix ui ci && npm --prefix ui run lint && npm --prefix ui run check && npm --prefix ui test && npm --prefix ui run build && python scripts/generate_openapi.py Expected: PASS.

  • Step 4: Commit docs: when to open a browser headless, the wiki pages inside docs/ with the pointer, and chore: changelog for headless.

Clone this wiki locally