UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

339 lines (242 loc) • 19.3 kB
# Deploy KXM Run one KXM [hub](../glossary.md#hub) and one [Runtime](../glossary.md#runtime) supervisor as a supervised service, on loopback or behind a trusted proxy. This page is for operators who take KXM past a single developer terminal. You end with a hub that restarts cleanly, clients bound to it with the right credential, and, for hosted use, one isolated box per tenant. ## Before you begin - Install the `kxm` CLI on the host ([Install KXM](../start/install.md)) and run `kxm init` in the project checkout. - Read the [trust model](../concepts/trust-model.md) if anyone other than you will hold a KXM credential. - Have a service manager (systemd, launchd, or a Windows service wrapper) and a place to keep secrets outside Git. ## Know the deployment envelope KXM is a single-node service. One Node.js process serves the hub over HTTP and server-sent events, and it is the only writer of one SQLite database. The Runtime supervisor is a second local process that owns run event stores and pushes summaries to the hub. | KXM provides | KXM does not provide | |---|---| | Durable restart recovery of agents, messages and workflow runs | Clustering, leader election or shared-state failover | | Project-scoped tokens and per-agent keys | Per-user identity, roles or an identity provider | | Signed webhook workflows and callbacks | Exactly-once delivery (it is at-least-once) | | Health, readiness, metrics and structured logs | TLS termination (the hub speaks plain HTTP) | | Graceful shutdown and PID-claim recovery | Encryption at rest (SQLite files are plain) | Do not put a load balancer in front of several hubs. Each hub has its own presence, queue and database, so agents on different hubs never see each other. ## Choose loopback or a network bind The hub listens on `KXM_HOST`, which defaults to `127.0.0.1`, and on `KXM_PORT`, which defaults to `7331`. Keep loopback unless a client on another machine must reach the hub. The hub process refuses a non-loopback bind without an admin token: ```text KXM_AUTH_TOKEN is required when binding beyond localhost ``` `kxm hub start` always supplies an admin token, generating one if needed, so that check alone protects nothing. Before you bind anywhere else, put all of the following in place: 1. An admin token and a distinct project token for every project (see [Manage credentials](#manage-credentials)). 2. A TLS-terminating reverse proxy that is the only public listener. 3. Network restriction in front of the proxy: an IP allowlist or a VPN. 4. Buffering disabled, and read timeouts above 15 seconds, for the event streams `/v1/events` and `/v1/ops/events`. The hub sends a heartbeat every 15 seconds. 5. Encrypted storage for the state directory, because message bodies are stored as sent. > [!WARNING] > Never expose the hub port directly to the internet. The built-in rate limit keys on a client-supplied agent header or the socket address, which is the proxy's address behind a proxy, and it resets on restart. It is a courtesy limit, not abuse protection. ## Manage credentials The hub checks two kinds of bearer token. Give each holder only the one it needs. | Credential | Who holds it | What it opens | |---|---|---| | Admin token (`KXM_AUTH_TOKEN` on the hub) | The hub and the operator terminal | `/metrics`, `/v1/ops/*`, admin context calls, state promotion, quorum degradation, and any project missing from the project-token map | | Project token (an entry in `KXM_PROJECT_TOKENS`) | Agents and the Runtime for that project | Registration, discovery, messaging, context, Runtime sync and assigned workflow operations for that project only | List every project in `KXM_PROJECT_TOKENS`. A project that is not in the map falls back to the admin token, so an admin-token holder can register agents in any unlisted project name. Webhook start and signal secrets are separate HMAC secrets; see [Run webhook workflows](../guides/webhook-workflows.md). ### How the hub finds its token `kxm hub start` resolves credentials in this order and never prints a token: 1. `KXM_AUTH_TOKEN` and `KXM_PROJECT_TOKENS` from the environment. Explicit values are also saved to the credential file so restarts keep them. 2. The credential file `hub-env.json` (schema `kxm.hub-env.v1`, mode `0600`) under the user state root. The root is `KXM_STATE_HOME`, or the platform default listed in [State outside the project](../reference/config-reference.md#state-outside-the-project). 3. A newly generated admin token, saved to that file. The hub therefore never starts without an admin token by accident. Expected output on first start: ```text kxm hub: using newly generated KXM_AUTH_TOKEN from /home/kxm/.local/state/kxm/hub-env.json ``` Operator clients on the same machine (`kxm dash`, the Runtime) read the same file. They use `KXM_AUTH_TOKEN` from their environment first, then the saved project token for their project, then the saved admin token. Agent clients (the Pi extension, the Claude Code MCP server, and the `kxm peer` and `kxm workflow` agent commands) never fall back to the admin token. To rotate the admin token, restart the hub with a new `KXM_AUTH_TOKEN`; the hub saves it in place of the old one and keeps the saved project tokens. Rotate a project token by restarting with an updated full `KXM_PROJECT_TOKENS` map. Then restart every client that held the old value. Deleting `hub-env.json` also works, but it drops the saved project tokens too. ### Generate and inject tokens Create tokens once, store them with your secret manager or in files readable only by the service account, and inject them through the environment. Never pass a token as a command-line argument. ```bash umask 077 mkdir -p /etc/kxm openssl rand -hex 32 > /etc/kxm/admin-token # One entry per project the hub serves node -e 'const t = () => require("node:crypto").randomBytes(32).toString("hex"); process.stdout.write(JSON.stringify({ product: t(), api: t() }))' > /etc/kxm/project-tokens.json ``` The map must list every project the hub serves. `/etc/kxm/project-tokens.json`: ```json {"product": "replace-with-product-token", "api": "replace-with-api-token"} ``` ## Start the hub `kxm hub start` runs in the foreground from the project checkout. It writes the PID claim `.kxm/state/hub.pid`, the database `.kxm/state/kxm.db`, and the log `.kxm/logs/kxm-hub.jsonl` unless you relocate them. ```bash cd /srv/kxm/product export KXM_HOST=127.0.0.1 export KXM_PORT=7331 export KXM_AUTH_TOKEN="$(cat /etc/kxm/admin-token)" export KXM_PROJECT_TOKENS="$(cat /etc/kxm/project-tokens.json)" # every project, not only this one kxm hub start ``` <details><summary>PowerShell</summary> ```powershell Set-Location C:\srv\kxm\product $env:KXM_HOST = "127.0.0.1" $env:KXM_PORT = "7331" $env:KXM_AUTH_TOKEN = Get-Content C:\kxm\secrets\admin-token $env:KXM_PROJECT_TOKENS = Get-Content C:\kxm\secrets\project-tokens.json -Raw kxm hub start ``` </details> > [!WARNING] > `KXM_PROJECT_TOKENS` replaces the hub's saved project map instead of adding to it, and the hub saves the replacement. A one-project value silently removes every other project, and their agents then fail with `invalid_auth`. Always pass the full map. To add a project to a hub started by hand, use the merge command in [Start the hub](../start/quickstart-claude-code.md#3-start-the-hub) and [Give the project a token on the running hub](../start/quickstart-claude-code.md#give-the-project-a-token-on-the-running-hub). Expected output: ```text kxm hub listening at http://127.0.0.1:7331; storage=/srv/kxm/product/.kxm/state/kxm.db; auth=token ``` Confirm from another terminal: ```bash kxm hub view ``` Expected output: ```text hub health=true ready=true · loopback hub ``` ## Bind clients to the hub Clients pick their hub in this order: `KXM_SERVER_URL`, then the machine binding written by `kxm hub bind`, then `http://127.0.0.1:7331`. The binding lives in `hub-binding.json` under the user state root and is labelled `loopback` or `remote`. ```bash # Same machine as the hub kxm hub bind http://127.0.0.1:7331 ``` Expected output: ```text bound hub http://127.0.0.1:7331 · loopback · health=on ``` A remote URL puts a bearer token on the network, so `kxm hub bind` refuses it when this machine has no credential for the project: ```text refusing to bind remote hub https://hub.example.com with no credential for project product; export KXM_AUTH_TOKEN (or point KXM_STATE_HOME at the hub-env record that already holds one), then re-run; the hub itself requires a token beyond loopback ``` Export the project token for this machine, then bind again. A successful remote bind ends with `· token leaves this machine`. Only `localhost`, names ending in `.localhost`, `::1` and `127.x.x.x` count as loopback; `0.0.0.0`, a LAN address or any other host name is remote. Remove a binding with `kxm hub unbind`. ## Supervise the hub and Runtime ### The hub Run `kxm hub start` under your service manager with a stable working directory, secrets injected from protected files, restart on failure, and a stop timeout of at least 15 seconds. On `SIGTERM` the hub stops accepting connections, closes event streams, waits up to 5 seconds for active requests, and closes SQLite; its wrapper force-stops the server after 10 seconds. Example `/etc/systemd/system/kxm.service` (illustrative, adjust paths and names): ```ini [Unit] Description=KXM hub After=network.target [Service] User=kxm WorkingDirectory=/srv/kxm/product Environment=KXM_HOST=127.0.0.1 Environment=KXM_PORT=7331 Environment=KXM_STATE_HOME=/srv/kxm/state # Lines KXM_AUTH_TOKEN=... and KXM_PROJECT_TOKENS='{...every project...}', mode 0600 EnvironmentFile=/etc/kxm/hub.env # Path from: command -v kxm ExecStart=/usr/local/bin/kxm hub start Restart=on-failure RestartSec=5 TimeoutStopSec=20 [Install] WantedBy=multi-user.target ``` The PID claim prevents a second hub on the same state directory. A claim whose wrapper died is reclaimed on the next start, and an orphaned server child is stopped first. `kxm hub stop` also stops long-lived workers that recorded claims. Set `KXM_DAEMON=1` to stop mirroring log lines to stdout when your service manager already reads the log file. The Pi extension starts a hub in the background when none is running (`hub.autoStart: background`). On a service host, set `hub.autoStart: off` in [personalization settings](../reference/config-reference.md#personalization-settings-kxmconfigv1) so only the service manager starts the hub. ### The Runtime supervisor The supervisor is a singleton per user state root. It always detaches from the command that starts it and has no foreground mode, so a service manager cannot own its process directly. Runtime commands such as `kxm run`, `kxm runs list` and `kxm runs cancel` start it on demand. 1. Start it after the hub with `kxm runtime start`. 2. Check it from a timer with `kxm runtime status`, which exits `1` when it is not running. 3. Stop it before the hub with `kxm runtime stop`. The command returns once shutdown starts; running drives get up to `KXM_RUNTIME_STOP_GRACE_MS` (default 30 seconds, maximum 10 minutes) to finish. > [!NOTE] > The supervisor inherits the environment of the command that started it, including `KXM_SERVER_URL` and `KXM_AUTH_TOKEN`. After you change either, stop and start the supervisor. A `kxm hub bind` change is picked up on the next sync tick. Long-lived Pi workers need one service unit each; see [Run supervised Pi workers](../guides/pi-workers.md). ## Host one tenant per box For a hosted service, a tenant is a machine, not a row. Each box runs one hub, one Runtime supervisor and one state set, and a separate multi-user web application (the portal) is the only thing users reach. The hub has no tenant column and no user accounts, so do not serve unrelated teams from one hub: the admin token and the shared database collapse all isolation. The following diagram shows that browsers stop at the portal, which calls the hub and Runtime over loopback. ```mermaid flowchart LR B[Browser] subgraph Box["Tenant box"] P["Reverse proxy<br/>TLS + Authentik forward auth"] PB["Portal backend<br/>users and sessions"] HUB[("KXM hub<br/>127.0.0.1")] RT["Runtime supervisor<br/>127.0.0.1"] P -->|"strips Authorization and x-kxm-* headers"| PB PB -->|"machine token over loopback"| HUB PB -->|"kxm tenant status"| RT RT -->|"sync events"| HUB end B -->|HTTPS| P B -.->|"blocked: no public hub or Runtime port"| HUB ``` Hold these invariants on every tenant box: 1. **A service account, not root.** Session isolation routes model context; it is not a sandbox against a hostile process under the same account. Give untrusted workers separate accounts or containers. 2. **Explicit, stable paths.** Set `KXM_WORKSPACE_DIR`, `KXM_STATE_DIR`, `KXM_DATA_PATH`, `KXM_LOG_PATH` and `KXM_STATE_HOME` instead of inheriting a home directory, and pin `KXM_HOST=127.0.0.1`. `kxm backup` ignores `KXM_STATE_DIR` and `KXM_DATA_PATH`, so on such a box it finds no hub store: it copies only the project's Runtime event store under `KXM_STATE_HOME`, or fails with `backup_no_stores` when there is none. Copy the hub database with the [stopped-state backup](backup-and-restore.md#back-up-everything-else). 3. **Loopback listeners only.** Neither the hub nor the supervisor has a public port, and nothing is load-balanced across hubs. 4. **Edge authentication.** The proxy authenticates browsers (for example with Authentik forward auth) on the portal's routes only. The hub never interprets browser identity; see [ADR-0004](../adr/ADR-0004-edge-identity-authentik.md). 5. **Server-side machine credentials.** The portal backend keeps the hub credentials and calls the hub itself. `kxm tenant status`, which uses the admin token, gives it one labelled view of hub metadata and Runtime runs. 6. **One restart path.** Use the service manager and PID-claim recovery above. Do not add a second manager for the hub. ### Reverse-proxy contract KXM ships no proxy configuration, because a generated file reads as authoritative while one missing directive can re-open header forgery. Whatever proxy you use, it must hold these properties: - The hub and supervisor ports are not reachable from outside the box. - The proxy strips client-supplied `Authorization`, `x-kxm-agent-id`, `x-kxm-agent-key`, `x-kxm-caller-id` and any identity or tenant headers before it adds its own validated values. - The proxy never injects the hub admin token on a user's behalf. That makes every authenticated user a hub admin and destroys attribution. - A browser never holds, echoes or is redirected with a hub bearer. - If outside systems must deliver signed webhooks, forward only `POST /v1/webhooks/...` paths to the hub. They authenticate with their HMAC signature; the rest of `/v1/*` stays private. Example (illustrative nginx shape; not generated, and not tested by KXM): ```nginx server { listen 443 ssl; server_name kxm.example.com; location /outpost.goauthentik.io { proxy_pass http://127.0.0.1:9000/outpost.goauthentik.io; proxy_pass_request_body off; proxy_set_header Content-Length ""; } location / { auth_request /outpost.goauthentik.io/auth/nginx; # Drop headers a browser could forge before they reach the portal. proxy_set_header Authorization ""; proxy_set_header X-Kxm-Agent-Id ""; proxy_set_header X-Kxm-Agent-Key ""; proxy_set_header X-Kxm-Caller-Id ""; # The portal backend, never the hub. proxy_pass http://127.0.0.1:8080; } } ``` ## Boot the kxmd Proxmox host The Proxmox host starts these guests in the order below. The VMID is the Proxmox guest id. `up` is the delay, in seconds, after that guest starts. | Order | Guest | VMID | Role | up | |---|---|---|---|---| | 1 | firewall | 200 | Firewall | | | 2 | gateway | 201 | Gateway | | | 3 | kxmd-pg | 220 | Postgres | 30 | | 4 | kxmd-temporal | 221 | Temporal | 40 | | 5 | kxmd-services | 300 | `kxm-control` | 30 | | 6 | kxmd-studio | 241 | Hub | 20 | VM 300 is named `kxmd-services`. VM 230 is named `kxmd-proxy` and runs Caddy. Steel is LXC 240 and uses Proxmox startup order 40. Steel's public name is `steel.kontextmind.com`. `steel.theneuro.me` is an alias of that same server. Clients reach it only through Caddy and Authentik forward auth. Direct LAN, tailnet, and host-forward access is blocked. See [ADR-0007](../adr/ADR-0007-steel-caddy-authentik.md) and [Browser automation](../guides/browser-automation.md). ## Name machine accounts New Authentik machine accounts follow [ADR-0006](../adr/ADR-0006-machine-account-names.md): | Scope | Name | |---|---| | Platform-wide | `svc-<system>-<purpose>` | | Tenant-scoped | `svc-<tenant>-<system>-<purpose>` | | Test, witness, or proof | The same shapes with a `test-` prefix | `<tenant>` is the tenant slug. A `test-` account is never in a production group. Outposts and `ak-*` accounts are Authentik-managed and are exempt. Names already in use, including `kxm-agent`, `kxm-witness-*`, `witness9`, and `kxmdproof`, wait for an approved inventory. Do not invent a new name for one of them. The live Steel account remains `svc-steel`. ## Plan for scale Measure concurrent agents, request rate, event-loop delay, database size, disk latency and reconnect frequency for your workload. Terminal messages are purged after `KXM_MESSAGE_RETENTION_MS` (7 days by default), and finished workflow runs and their journals after 7 days. Keep free disk space ahead of the database's growth, and include workflow data in privacy reviews: verified evidence snapshots stay in a run after its source messages are purged. Durable transport does not make peer work exactly-once. Use idempotent tasks and stable idempotency keys, and keep important artifacts in Git or another system of record. ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `KXM_AUTH_TOKEN is required when binding beyond localhost` | The hub process was started directly with a non-loopback `KXM_HOST` and no admin token | Set `KXM_HOST=127.0.0.1`, or finish the network-bind checklist and set `KXM_AUTH_TOKEN` | | `KXM hub is already managed by PID <pid>` | A live hub already owns this state directory | Run `kxm hub stop`, or use a different workspace | | `hub_bind_unauthenticated` from `kxm hub bind` | Remote URL and no credential for the project | Export the project token, then bind again | | Agents fail with `invalid_auth` after a restart | `KXM_PROJECT_TOKENS` changed or dropped a project | Pass the full map again and restart the hub | | Event streams stall behind the proxy | Response buffering or a short read timeout | Disable buffering on `/v1/events` and `/v1/ops/events`; raise the read timeout | See [Troubleshoot KXM](troubleshooting.md) for more. ## Next steps - Watch the service: [Monitor KXM](monitoring.md) - Protect its state: [Back up and restore KXM](backup-and-restore.md) - Move to a new release: [Upgrade KXM](upgrade.md) - Keep run facts flowing to the hub: [Operate Runtime sync and leases](runtime-sync.md) - Understand who can do what: [Trust model](../concepts/trust-model.md) - Name machine accounts: [ADR-0006](../adr/ADR-0006-machine-account-names.md) - Reach Steel: [ADR-0007](../adr/ADR-0007-steel-caddy-authentik.md) - Every hub route: [Hub HTTP API reference](../reference/http-api.md)