Driving without an agent
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.
The HTTP interface
Section titled “The HTTP interface”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:
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.
Input observation
Section titled “Input observation”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.cursorreturns the newest event number.input.wait_forwaits for text, a key chord or a click since a given cursor, and returnsmatched, the textcapturedand 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.
File transfer
Section titled “File transfer”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.
Example: a Playwright test
Section titled “Example: a Playwright test”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.