Kernel API overview
The Nuclei kernel is a standalone WebSocket service. The desktop frontend is just its first client — anything that can open a WebSocket and speak JSON can parse circuits, run simulations, and submit hardware jobs. This section documents that protocol as a public API.
Connection
Section titled “Connection”| Property | Value |
|---|---|
| URL | ws://localhost:9742 |
| Port fallback | If 9742 is taken, the kernel scans upward through 9742–9761 (range of 20) and binds the first free port |
| Framing | JSON text frames — one request or response object per frame |
| Max inbound message | 1 MiB (1_048_576 bytes); larger frames close the connection |
| Heartbeat | WebSocket ping every 30 s; connection dropped after a 20 s pong timeout |
| Auth | Per-launch token, presented in the first frame (see Authentication) |
Requests on one connection are processed strictly in order: the handler
fully finishes (and sends every response for) request N before reading
request N + 1. For concurrent work, open multiple connections — each
connection gets its own isolated Executor (parse/execute state never
bleeds between clients), while hardware state (provider connections, job
registry) is shared process-wide.
Authentication
Section titled “Authentication”The kernel executes code it receives, so every connection must authenticate
before any request is handled. This is protocol v2 and it is breaking for
external clients: a client that does not send the authenticate frame first
is disconnected. See the protocol changelog.
- The kernel reads a secret token on startup. The desktop app generates a
fresh one per launch and writes it to the kernel’s stdin
(
NUCLEI_KERNEL_TOKEN_STDIN=1), never into its environment; scripts that launch a kernel themselves can setNUCLEI_KERNEL_TOKENinstead. With no token it refuses to start. - It writes the token and port to user-only (
0600, directory0700) runtime files in~/.nuclei/runtime/(override the directory withNUCLEI_RUNTIME_DIR), so local scripts can read them. Each kernel writes its ownkernel-<port>.json(for examplekernel-9742.json), and alsokernel.json, a pointer to the most recently started kernel (last writer wins). With several kernels running, readkernel-<port>.jsonfor the port you are connecting to;kernel.jsonis only right when you want “the newest kernel”. Both files hold{"url", "port", "pid", "token"}, are written atomically, and are removed on clean shutdown (SIGTERM/SIGINT). Check thatpidis still alive before trusting a file left behind by a crash. - The first frame on a connection must be
{"type": "authenticate", "token": "<token>", "nonce": "<64 hex chars>"}. The kernel answers{"type": "authenticated", "proof": "<hex>"}and only then handles requests. The token is never sent in the URL.nonceis optional for third-party clients (see Server proof). - Browser connections must also come from an allowed
Origin, and theHostheader must be a loopback name on the kernel’s port. Scripts that send noOriginare accepted only with the token.
Every socket the app opens authenticates this way: the editor’s, the Trainability Lab’s, the Discovery Engine’s, and the audit and conjecture sessions. Replies in the Quantum AI Lab messages examples start after the handshake.
Server proof
Section titled “Server proof”Anything can listen on a free port, so the kernel also proves it knows the
token. The client sends a fresh random nonce (32 random bytes as 64 hex
characters, 16 to 256 characters accepted) in the authenticate frame. The ack
then carries
proof = hex( HMAC-SHA256( key = token, message = nonce ) )where token and nonce are the strings exactly as sent (UTF-8, the nonce is
not hex-decoded). The client must compare proof in constant time and close
the connection without sending anything else when it is missing or wrong.
- Nuclei’s own client always sends a nonce and requires the proof. It holds back every other frame (your code, buffers, credentials) until the proof verifies, and treats a missing or wrong proof as a failed kernel.
- A kernel started by a Nuclei version before 0.14.1 answers without a proof, so the app refuses it.
- For third-party clients the nonce stays optional: a frame without
nonceis still accepted and acknowledged without aproof. A frame with a malformednonce(not a string, or outside 16 to 256 characters) closes with4401. Sending a nonce and checking the proof is recommended; see client examples.
How a connection is refused
Section titled “How a connection is refused”| Situation | Result |
|---|---|
Foreign Origin or non-loopback Host | HTTP 403 on the WebSocket upgrade; the connection never opens |
First frame is not a valid authenticate frame, wrong token, malformed nonce, or no frame within 3 s | Closed with code 4401 |
| Frame larger than 8 KiB before authentication | Closed with code 1009 (the normal 1 MiB limit applies once authenticated) |
| More than 32 simultaneous unauthenticated connections | The extra connection is closed at once with code 4429; wait a moment and reconnect |
Request sent before the authenticated ack | Not handled; the connection is closed with 4401 |
A client library’s “connection refused with 403” therefore means the Origin
or Host is wrong, and a 4401 close means the token is wrong or missing.
Threat boundary
Section titled “Threat boundary”The token protects the kernel from web pages and from other users on the machine. It does not protect it from other processes running as the same operating-system user: the runtime files are readable by that user (that is how local scripts authenticate), so any process running as you can read the token and run code through the kernel, and could equally run that code directly. This is the stated threat boundary, documented rather than defended: do not treat the token as a sandbox against software you already run as yourself.
When a client is rejected (WebSocket close code 4401), the token it presented
did not match the kernel it reached. The Nuclei app shows “Kernel rejected the
session token — restart Nuclei” instead of retrying.
For throwaway local experiments only, NUCLEI_KERNEL_INSECURE_DEV=1 disables
authentication and prints a warning. The kernel never passes its token to the
processes it starts (code execution, Lab workers, the discovery sandbox).
Running the kernel standalone
Section titled “Running the kernel standalone”cd kernel && NUCLEI_KERNEL_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" python server.pyOn success the kernel prints exactly one ready line to stdout:
Nuclei kernel ready on ws://localhost:9742This line is the port-discovery contract: when the default port was taken, the chosen port appears in this line, on its own line, flushed — parse it rather than assuming 9742.
The streaming model
Section titled “The streaming model”A request does not get a reply. It produces an ordered sequence of
response messages on the same connection, ending with a request-specific
final message (result for execute, python_result for run_python,
snapshot for parse — though a failed parse appends one error after
it — and a single reply for every hardware_* request). Streamed output / stderr messages may interleave before the
terminal message, so a robust client loops until it sees the terminal type
— see client examples.
The canonical example — execute on a Bell circuit produces two messages,
snapshot then result:
{ "type": "execute", "code": "from qiskit import QuantumCircuit\n\nqc = QuantumCircuit(2, 2)\nqc.h(0)\nqc.cx(0, 1)\nqc.measure([0, 1], [0, 1])\n", "shots": 1024, "language": "python"}[ { "type": "snapshot", "data": { "framework": "qiskit", "qubit_count": 2, "classical_bit_count": 2, "depth": 3, "gates": [ { "type": "H", "targets": [0], "controls": [], "params": [], "layer": 0 }, { "type": "CNOT", "targets": [1], "controls": [0], "params": [], "layer": 1 }, { "type": "Measure", "targets": [0], "controls": [], "params": [], "layer": 2 }, { "type": "Measure", "targets": [1], "controls": [], "params": [], "layer": 2 } ] } }, { "type": "result", "data": { "state_vector": [ { "re": "<approx:0.70710678:0.0000001>", "im": "<approx:0:0.0000001>" }, { "re": "<approx:0:0.0000001>", "im": "<approx:0:0.0000001>" }, { "re": "<approx:0:0.0000001>", "im": "<approx:0:0.0000001>" }, { "re": "<approx:0.70710678:0.0000001>", "im": "<approx:0:0.0000001>" } ], "probabilities": { "00": "<approx:0.5:0.000001>", "11": "<approx:0.5:0.000001>" }, "measurements": "<any>", "bloch_coords": [ { "x": "<approx:0:0.000001>", "y": "<approx:0:0.000001>", "z": "<approx:0:0.000001>" }, { "x": "<approx:0:0.000001>", "y": "<approx:0:0.000001>", "z": "<approx:0:0.000001>" } ], "execution_time_ms": "<any>", "shot_count": 1024, "metrics": {} } }]Values like "<any>" and "<approx:0.5:0.000001>" are matcher markers, not
protocol values: every example in this section is a fixture file that the
kernel test suite replays against the real handler
(kernel/tests/test_docs_fixtures.py), and markers absorb nondeterminism
(sampled counts, timings). The shapes and orderings you read here are
machine-checked on every CI run.
Message catalog
Section titled “Message catalog”Request type | Terminal response | Reference |
|---|---|---|
parse | snapshot (an error follows it on failure) | Execution messages |
execute | result | Execution messages |
run_python | python_result | Execution messages |
environment | environment | Execution messages |
hardware_connect | hardware_connected | Hardware messages |
hardware_set_credentials | hardware_connected | Hardware messages |
hardware_clear_credentials | hardware_connected | Hardware messages |
hardware_connected_providers | hardware_connected_providers | Hardware messages |
hardware_list_jobs | hardware_jobs | Hardware messages |
hardware_list_backends | hardware_backends | Hardware messages |
hardware_submit | hardware_job_submitted | Hardware messages |
hardware_status | hardware_job_update | Hardware messages |
hardware_results | hardware_result | Hardware messages |
hardware_cancel | hardware_job_cancelled | Hardware messages |
Any other type (and any frame that is not valid JSON) produces a bare
error message — see errors. Payload schemas
are in schemas. The TypeScript mirror of the
whole protocol — the types the frontend itself compiles against — lives at
src/types/quantum.ts.