Skip to content
Draft, pre-release documentation

A test harness or a script can use Kasm Session Runtime with no agent and no model in the loop. Three features are for that caller: a plain HTTP interface to every method, a record of the input the session receives from any source, and file transfer in and out of the session.

The typical case is a browser test, written with Playwright for example, that drives Kasm’s own viewer the way a person would and needs to prove that its clicks and keystrokes reached the session and the application inside it.

None of the three needs a model. Only screen.judge, screen.ground and screen.ask do, and they return an error when no model is configured.

When KASM_RUNTIME_SESSION_TOKEN is set, the runtime’s mcp mode serves plain HTTP beside the MCP endpoint, on the same listener and behind the same token. Each method is one POST, with its parameters as the JSON body:

Terminal window
curl -s -X POST -H "X-Kasm-Runtime-Session-Token: $TOKEN" \
-d '{"chord": "ctrl+s"}' \
https://<host>/desktop/<kasm_id>/mcp/v1/call/input.key
# {"result":"pressed ctrl+s"}

The token goes in the X-Kasm-Runtime-Session-Token header, because Kasm’s reverse proxy overwrites Authorization. The same 28 methods are available as over MCP, and a call records the same row in report.json.

Status Body When
200 {"result": ...} the method ran
400, 404, 413 {"error": {"class": "malformed", ...}} bad parameters, no such method, body over 8 MiB
422 {"error": {"class": "resolution", ...}} not found, timed out, capability unavailable; worth retrying or trying something else
500 {"error": {"class": "environment", ...}} the session is broken; not worth retrying
503 {"error": {"class": "shutting_down", ...}} the server is stopping

GET /v1/openapi.json returns an OpenAPI 3.1 document for every route on the listener, with the credential each one takes. It needs no token. The full list is in the HTTP API reference.

With KASM_RUNTIME_OBSERVE_INPUT=true, the runtime records every key and mouse button press and release the session’s X server receives, whatever sent it: the Kasm viewer, the runtime itself, or another program. It works in run and mcp modes and is off by default. Recording never changes delivery: every keystroke and click reaches the session as it would with recording off.

Each event is an input event on /events and in trace.jsonl, marked origin: "external" or "runtime". Two methods use the record:

  • input.cursor returns the newest event number.
  • input.wait_for waits for text, a key chord or a click since a given cursor, and returns matched, the text captured and the events.

Take the cursor first, then act, then wait. Otherwise input that arrives before the wait starts is missed.

input.cursor() -> {"cursor": 17}
... the test clicks and types in the viewer ...
input.wait_for({"since": 17, "text": "hello", "timeout_ms": 5000})
-> {"matched": true, "captured": "hello", ...}

On timeout the call returns normally with matched: false. timeout_ms defaults to 5000 and is capped at 60000. The recorder keeps the last 4096 events.

A match proves that the input reached the session’s X server. Whether the intended application received it depends on the focused window, so pair the wait with a check on the application’s state, such as dom.wait or a11y.tree, and type text unique to the test so an earlier run’s input cannot satisfy it.

Keystrokes are recorded verbatim, so turning input observation on also forces KASM_RUNTIME_OBSERVE_TOKEN_REQUIRED: every read of /events, /artifacts/ and the inbox then needs the session token or the messaging token. The exception is the runtime’s own typing, which may carry credentials from the scenario. Key events recorded while the runtime types carry no key or text, only private: true, and a person’s keystrokes that land during that time are hidden the same way.

It relies on the X server’s RECORD extension, which KasmVNC’s Xvnc 1.5.0 provides. Where it is missing, the info event reports input_observation: "unavailable" and the methods say so.

Files move through two roots only: the session user’s home directory and /tmp. A path is written ~, ~/<path>, /tmp or /tmp/<path>, and cannot escape either root, including through a symlink.

Route or method What it does
GET /v1/files/{home,tmp}/<path> download a file, streamed, with a SHA-256 trailer
PUT /v1/files/{home,tmp}/<path> upload a file; ?overwrite=true, ?mode=0755; 409 if something is there without overwrite
GET /v1/dirs/{home,tmp}/<path> download a directory as a zip, streamed
PUT /v1/dirs/{home,tmp}/<path> upload a zip and extract it; the whole archive is checked before anything is written
file.get, directory.get the same reads over MCP or /v1/call/, returned inline as base64 up to 5 MiB

Uploads are HTTP only. One upload may send up to KASM_RUNTIME_UPLOAD_MAX_BYTES and one archive may extract to KASM_RUNTIME_EXTRACT_MAX_BYTES, both 1 GiB by default. At most 4 transfers run at once, and a stream idle for 60 s is cut off. Transfers need the session token; the messaging token a sidebar holds is refused. They are recorded in the report’s transfers list with path, size and digest; content is not recorded. File transfer is available to outside callers in mcp mode. The runtime’s own model has no file-transfer tool.

The 10-playwright-verify example in the SDK is a Playwright test against a real Kasm session. It clicks and types on the KasmVNC canvas, asserts with input.wait_for that the click reached the X server at the intended pixel and the text arrived, then confirms with dom.wait that the page’s text box holds the text, all over /v1/call/.

The example found a fault on 2026-09-24: when the browser showed the viewer at any size other than 1:1, the click reached the X server at its page pixel rather than the screen pixel it was aimed at. The example fails when that happens. See Known issues.