Running the Runtime
What kx serve opens — the gRPC endpoint, the event stream, and the optional web console — and the auth posture it refuses to start without.
kx serve starts the agentic runtime as a server on your machine. It is the thing your apps, your agents, the CLI, and the web console all talk to. One command starts all of it.
Auth is deny-all by default
The runtime answers nobody until you tell it who to answer. A bare kx serve does not start an open server — it stops before binding anything and exits with status 2, because starting an unauthenticated server would be the wrong default.
Start it with no auth flags at all and it prints the usage text and this line on standard error:
kx: kx serve refuses unauthenticated access. Re-run with --dev-allow-local (loopback-only dev access), or --auth-token <token>=<party> / --auth-token-file <path> for token auth.You have two ways to open it. Pick exactly one — the loopback dev posture and configured tokens are mutually exclusive, and passing both is a config error.
# Local development: accept callers from this machine only.
kx serve --dev-allow-local
# Token auth: a bearer token maps to a named party.
kx serve --auth-token-file ~/.kortecx/tokens.txt# You start the server from a terminal; code connects to it.
from kortecx import KxClient
with KxClient("http://127.0.0.1:50151") as kx:
result = kx.invoke("kx/recipes/echo", {"topic": "hello"}, wait=True)
print(result.state)
# The demo recipe returns binary bytes, so result.text is None here.
print((result.bytes or b"")[:32])// You start the server from a terminal; code connects to it.
import { KxClient, type Result } from "@kortecx/sdk";
const kx = new KxClient("http://127.0.0.1:50151");
const result = (await kx.invoke(
"kx/recipes/echo",
{ topic: "hello" },
{ wait: true },
)) as Result;
console.log(result.state, result.instanceId);
kx.close();What --dev-allow-local actually means
--dev-allow-local accepts any caller that reaches the server over loopback — the local machine. If you pass it, both --listen and --ws-listen must be loopback addresses or the server refuses to start. It is for development on your own computer. For anything a second machine can reach, use a token.
What it opens
One kx serve opens up to three surfaces. Every resolved path and endpoint is written to a startup log line on standard error (tracing is on at info by default), so you can read back exactly what resolved where.
| Surface | Default address | What it is |
|---|---|---|
| gRPC (and gRPC-web) | 127.0.0.1:50151 | the main endpoint — the CLI and the SDKs talk here |
| Event stream | ws://127.0.0.1:50152 | a live WebSocket feed of what runs are doing |
| Web console | http://127.0.0.1:8888 | the browser interface, in builds that include it |
The event stream uses the same auth posture as the gRPC endpoint: an upgrade that carries no accepted token is rejected before any stream opens.
The console is a build-time option
The web console is a non-default cargo feature (console). The prebuilt release binary is built with it and carries the interface inside the binary — no separate install, no Node toolchain. A plain cargo install build has no console: it serves nothing at 127.0.0.1:8888, and passing --console-listen on such a build is a loud config error telling you to use the prebuilt binary or build with --features console.
Where the console is present it binds loopback only — a non-loopback --console-listen is refused. Open http://127.0.0.1:8888 in a browser on the same machine and point it at http://127.0.0.1:50151. If you use a bearer token, it is held only in the page's memory and is never written to disk.
Turn the console off with --no-console, or move it to another loopback port with --console-listen <addr:port>.
Starting it, end to end
Start the server
kx serve --dev-allow-localLeave it running. The journal, content store, and catalog resolve automatically under ~/.kortecx and are reused across restarts, so your history survives a stop and start.
Check it is answering
In a second terminal:
kx health
kx infoNeither takes a required flag of its own — only the shared client flags below. kx health is a liveness check against the gRPC endpoint and is not behind the auth interceptor, so it needs no token. kx info prints the non-secret server configuration and does need an accepted caller: on a token server, pass --token-file.
Run something
kx invoke kx/recipes/echo --args '{"topic":"durable agents"}' --waitThis runs a single-step blueprint and waits for the committed result.
Watch it live
kx events --instance <instance-id> --followThe instance id is the 32-character hex value kx invoke prints. Or open the web console and watch the same events there.
Every flag
These are the flags kx serve accepts.
Auth posture — one of these is required
| Flag | Default | Meaning |
|---|---|---|
--dev-allow-local | off | accept loopback callers; requires loopback --listen and --ws-listen |
--auth-token <token>=<party> | — | accept a bearer token as a named party (repeatable) |
--auth-token-file <path> | — | one token=party per line, # for comments |
--dev-allow-local and the token flags cannot be combined. Prefer --auth-token-file: a token passed on the command line is visible to anyone who can list processes on the machine.
Addresses
| Flag | Default | Meaning |
|---|---|---|
--listen <addr:port> | 127.0.0.1:50151 | the gRPC and gRPC-web endpoint |
--ws-listen <addr:port> | 127.0.0.1:50152 | the live-event WebSocket bridge |
--console-listen <addr:port> | 127.0.0.1:8888 | the web console; loopback addresses only |
--no-console | — | do not serve the web console |
Browsers and transport
| Flag | Default | Meaning |
|---|---|---|
--cors-origin <origin> | deny all | allow one browser origin on the gRPC-web shim (repeatable) |
--tls-cert <pem> / --tls-key <pem> | plaintext | in-binary TLS for the gRPC listener; give both or neither |
--cors-origin takes a full origin — https://dashboard.example.com, scheme and host and port. It is repeatable and it never accepts a wildcard. When the console is running, the runtime adds the console's own bound loopback origins (http://127.0.0.1:<port> and http://localhost:<port>, at whatever port it actually bound) to the allowlist automatically, so you do not need a --cors-origin for it — including when you moved it with --console-listen.
TLS covers the gRPC listener. The WebSocket bridge and the web console are plaintext — if a remote browser needs the event stream, put a TLS proxy in front.
Storage
| Flag | Default | Meaning |
|---|---|---|
--journal <path> | under ~/.kortecx | the durable journal |
--content <dir> | under ~/.kortecx | the content store |
--catalog-dir <dir> | beside the journal | catalog and sidecars (blueprints, teams, telemetry, capture) |
--content-max-bytes <N> | 32 MiB | the PutContent payload cap, enforced fail-closed |
All or nothing
The zero-config layout fires only when you pass none of --journal, --content, or --catalog-dir. Pass any one of them and you own the whole layout. To move everything at once, set KX_DATA_DIR instead.
Work and observation
| Flag | Default | Meaning |
|---|---|---|
--workers <N> | 1 | embedded worker pool size; above 1, Pure, IO and tool steps run concurrently |
--max-lease <N> | 16 | how many steps a worker leases at a time |
--metrics-listen <addr:port> | off | an opt-in Prometheus /metrics endpoint (RED metrics — rate, errors, duration) |
--webhook-listen <addr:port> | off | an opt-in inbound webhook surface for event triggers, with per-trigger HMAC or bearer auth |
--audit-log <path> | off | a best-effort JSONL record of the run lifecycle |
--workers must be a positive integer and is clamped to a built-in maximum. Start at 1 and raise it if your runs have several independent Pure, IO or tool steps ready at the same time.
The audit log is written off the truth path. It records join keys only — never payload bytes, never model output, never secrets — and turning it on never changes a run's committed facts or its product digest.
Environment variables
| Variable | Applies to | Meaning |
|---|---|---|
KX_DATA_DIR | kx serve | base directory for the zero-config data layout (default ~/.kortecx) |
KX_WORKERS / KX_SERVE_WORKER_POOL | kx serve | worker pool size when --workers is absent; the flag always wins |
KX_SERVE_MEMORY | kx serve | enable the durable memory RPCs and the kx memory surface |
KX_DATA_DIR=/data/kortecx kx serve --dev-allow-localKX_SERVE_MEMORY is the older spelling of the flag and still works; KX_FLAG_SERVE_MEMORY is the current name and wins if both are set.
Durable memory is gated. It needs KX_SERVE_MEMORY=1, a served model, and an hnsw build — see Memory. Datasets require an hnsw build too.
A server two machines can reach
If a second machine or a browser on another host needs to reach the runtime, three things change together.
Use tokens, not --dev-allow-local
Write one token=party per line. Blank lines and lines starting with # are ignored, and any other line that is not token=party is a config error:
# ~/.kortecx/tokens.txt
a-long-random-string=alice
another-long-random-string=ci-runnerBind a reachable address and turn on TLS
kx serve \
--auth-token-file ~/.kortecx/tokens.txt \
--listen 0.0.0.0:50151 \
--tls-cert /etc/kortecx/cert.pem \
--tls-key /etc/kortecx/key.pemOnly the gRPC listener moves here. The event stream stays on its own loopback default unless you also pass --ws-listen, and the console cannot be moved off loopback at all.
Name the browser origins you allow
kx serve \
--auth-token-file ~/.kortecx/tokens.txt \
--listen 0.0.0.0:50151 \
--tls-cert /etc/kortecx/cert.pem \
--tls-key /etc/kortecx/key.pem \
--cors-origin https://dashboard.example.comEach origin you want is its own --cors-origin. There is no wildcard.
Read this before you expose a port
Bearer tokens the runtime holds are compared with an ordinary map lookup, not a constant-time one, so a timing side-channel is possible in principle. This is a stated limitation of the single-system local gateway. A token in a browser page is visible to that page's JavaScript. See Production for what this does and does not cover.
Clients
Every client command — kx invoke, kx events, kx projection, and the rest — takes the same connection flags:
| Flag | Default | Meaning |
|---|---|---|
--endpoint <url> | http://127.0.0.1:50151 | which server to talk to |
--token <t> / --token-file <p> | — | the bearer token; prefer the file |
--tls-ca <pem> | — | the CA to trust for an https:// endpoint |
--json | off | machine-readable output |
kx invoke kx/recipes/echo --args '{"topic":"hello"}' --wait \
--endpoint https://runtime.example.com:50151 --token-file ~/.kortecx/my-tokenWhat this is not
The runtime is single-system by default. One kx serve is one machine: its journal, its content store, its workers. Running across several nodes is on the roadmap, not in the box today. The server runs for exactly as long as you leave it running — it does not stop or start itself, and it does not add or remove capacity on its own.
Scheduling and Triggers
Make a run start itself — on a clock, when a webhook lands, or on an authenticated gRPC call — with signed inbound requests, replay dedup, and a dry run that rehearses the wiring without firing it.
Giving It Context
Four ways to give an agent the material it needs — one-off files, a reusable brief, a searchable corpus, and facts that persist across runs — and how to choose between them.