# kern — full documentation > every kern doc concatenated for one-shot agent ingestion. > index: https://kern.aaenz.no/llms.txt · skill: npx skills add ellipog/kern-web # kern docs kern (always lowercase) is a **cross-platform desktop server manager** built with tauri v2 (rust backend + react frontend). it turns any folder on your computer into a managed server instance — with a live terminal, telemetry, and graceful lifecycle — and you teach it new server types by installing plugins. think of it as a self-hosted alternative to cloud game-panel tools (pterodactyl, amp, pufferpanel) — but as a **native desktop app** driven by a **plugin system**. ## where to start - **new to kern?** start with `getting-started` — download, register your first instance, start it. - **running servers daily?** `cli` for the terminal and dashboard, `backups` and `tasks` for the overnight safety net. - **building a plugin?** read `manifest-reference` first, then `lifecycle`, `config-schema`, and `plugin-ui`. - **writing scripts?** `automation-api` is the full JSON surface, `cli` is the friendly client. - **publishing?** see `distribution` for the registry flow. - **stuck?** `troubleshooting` maps symptoms to fixes; `recipes` has copy-paste setups. ## the big ideas - **instance** — a registered project folder that kern manages. full crud, with orphaned-state detection. - **plugin** — a `.kern` file that teaches kern how to run one type of server. - **lifecycle** — `start` / `stop` / `restart` / `install`, declared per-plugin and resolved at launch by rust. - **host api** — the bridge plugins use to talk to tauri commands, events, and the ui shell. > **note** these docs are the source of truth for plugin developers. the app itself ships with two sample plugins — a game server plugin and a bot runner — that exercise nearly every feature. ## beyond the basics - **preflight** — before a start, kern checks ports the instance used last time (with the owning pid), a pending minecraft eula, and low disk space. - **crash watchdog** — auto-restart with backoff after unexpected exits, plus a last-crash report (exit code + log tail) on the monitor tab. - **notifications** — in-app center, native os toasts when unfocused, discord/slack webhooks, and regex log alerts. see `notifications`. - **schedules** — interval, daily, or cron tasks (restart / start / stop / command / backup / health) with optional pre-restart console announcements. - **audit log** — local history of lifecycle actions, config changes, plugin installs, backups, and task runs. - **remote access** — a full control panel in a browser (console, files with a real editor, backups, schedules, plugins), on your lan over https or anywhere through a cloudflare tunnel, with per-device roles you can invite and revoke. see `web-remote`. - **cli & automation** — `kern-cli` and a loopback-only json api for scripts. see `cli`. # getting started ## 1. download grab the latest build from the [download section](/#download). - **windows** — `kern-setup.exe`, a **per-user installer**: no admin or uac prompt, installs to `%localappdata%\kern`, creates start menu and optional desktop shortcuts, and registers `.kern` file associations and the `kern://` protocol so double-clicking a plugin package just works. it also installs `kern-cli`. - **macos** — apple silicon `.dmg` (no intel build yet). gatekeeper may refuse the first launch: right-click → *open*. - **linux** — `.appimage` (no `.deb` yet). the builds are not os-code-signed. on windows, smart screen shows "windows protected your pc" the first time — choose *more info → run anyway*. after that, the app updates itself in place: updates are minisign-signed and verified before install. for the command line — including the full-screen dashboard (run `kern-cli` with no arguments) — and the automation api, see `cli`. ## 2. register a folder as an instance a "server instance" is just a project folder kern knows about. point kern at a directory and it becomes a managed instance — no daemons, no config files, no docker. already have a server folder? the register flow has an **import** option: pick the folder and kern inspects it (jars, launch scripts, `server.properties`, `eula.txt`) and pre-fills the plugin runtime for you. nothing is moved or modified. if a folder is moved or deleted, the instance is flagged **orphaned** rather than silently dropped, so you always know what kern thinks exists. ## 3. install a plugin the app is intentionally generic. **plugins teach it how to run each type of server.** install a plugin for the server type you want to run — try the game server plugin for a minecraft server, or `discord_bot` to scaffold and run a bot. plugins ship as `.kern` files (a zip with a `manifest.json` and an optional `dist/` ui bundle). double-clicking a `.kern` file opens kern via the `kern://install` deep link. ## 4. start, watch, stop 1. pick the instance, pick the plugin. 2. fill the config form (rendered dynamically from the plugin's `configSchema`). 3. hit `start`. stdout/stderr stream live to the terminal, appended to `/latest.log`, with full ansi color. 4. the input box is a command dispatcher: `start` / `stop` / `restart` / `install` trigger lifecycle; anything else is piped to stdin. 5. `stop` triggers a **graceful shutdown first** — kern sends the stop command, waits the instance's timeout (30 seconds by default, configurable per instance), then force-kills the whole process tree if it hasn't exited. a forced stop shows as `stopped-forced`. > **note** per-process telemetry (cpu + ram via `sysinfo`) shows as a reactor channel bar that turns amber above 90% cpu and red on fault. when a server exits unexpectedly, the monitor shows a **last crash** card with the exit code and the final log lines. # manifest reference a `.kern` file is a **zip archive** containing `manifest.json` (required), an optional `dist/index.js` esm ui bundle, an optional `dist/index.css`, and any other assets the bundle references. the manifest declares metadata, a dynamic config form, lifecycle commands, starter files, and ui tabs. ## full example ```jsonc { "id": "web_api", "displayName": "Web API Server", "version": "1.0.0", "author": "ellipog", "description": "Run a Node.js/Express web API with env-based config, health checks, and pm2 or nodemon for hot reload.", "uiEntry": "dist/index.js", "configSchema": [ { "key": "port", "label": "Port", "type": "text", "default": "3000" }, { "key": "log_level", "label": "Log Level", "type": "select", "options": ["debug","info","warn","error"], "default": "info" } ], "lifecycle": { "start": { "command": "node", "args": ["{{userOverrides.entry}}"] }, "start.nodemon": { "command": "nodemon", "args": ["{{userOverrides.entry}}"], "useShell": true } }, "scaffold": { "env": { "path": ".env", "content": "PORT={{userOverrides.port}}\nLOG_LEVEL={{userOverrides.log_level}}\n" }, "readme": { "path": "README.md", "content": "Web API ({{userOverrides.port}})…" } }, "tabs": [ { "id": "setup", "label": "Setup" }, { "id": "logs", "label": "Logs" } ] } ``` ## field reference | field | type | required | notes | |---|---|---|---| | `id` | string | yes | unique, lowercase, underscores. the registry key. | | `displayName` | string | yes | human name. | | `version` | string | yes | semver. | | `author` | string | yes | a github handle. the official plugins are published by `ellipog`. | | `description` | string | yes | one-liner for cards/detail. | | `uiEntry` | path | no | `dist/index.js` — the esm bundle exposing `mount(hostApi)`. | | `configSchema` | field[] | no | the dynamic config form (see `config-schema`). | | `lifecycle` | object | no | named lifecycle steps (see `lifecycle`). | | `scaffold` | object | no | starter files written into a fresh instance (see `scaffold`). | | `tabs` | tab[] | no | declarative tabs the plugin registers in the detail view. | > **warn** `id` must be lowercase with underscores only. `web_api` is valid; `Web-API` is not. # lifecycle the `lifecycle` block declares named steps. each step has a `command`, an `args` array, and an optional `useShell` flag. commands are resolved at launch by rust. ## templating commands and args support `{{userOverrides.*}}` templating, resolved from the config form the host rendered (see `config-schema`): ```jsonc "lifecycle": { "start": { "command": "{{userOverrides.java_path}}", "args": ["{{userOverrides.jvm_args}}", "-jar", "{{userOverrides.server_jar}}", "--nogui"] } } ``` ## runtime-qualified keys some plugins need different commands per runtime. **runtime-qualified keys** win when an override matches. the dotted form `start.` overrides the base `start`: ```jsonc "lifecycle": { "start": { "command": "node", "args": ["{{userOverrides.entry}}"] }, "start.bun": { "command": "bun", "args": ["{{userOverrides.entry}}"] }, "start.deno": { "command": "deno", "args": ["run", "--allow-net", "{{userOverrides.entry}}"] }, "start.rust": { "command": "{{userOverrides.binary}}", "useShell": true } } ``` the discord bot plugin uses exactly this pattern across its four runtimes (`node`, `bun`, `deno`, `rust`). ## useShell set `useShell: true` to run the command through a shell. needed for some installers (forge) that expect shell semantics. ## graceful shutdown `stop` sends the graceful shutdown first (stdin command and/or your plugin's `stop` step) and waits the instance's timeout before hard-kill. the timeout defaults to **30 seconds** and is configurable per instance (`stopTimeoutSecs`); the whole process tree is terminated after that, and the instance is reported as `stopped-forced`. > **warn** don’t put destructive commands in `stop`. the host will hard-kill after the timeout if your command hasn’t returned, but you should design `stop` to return promptly (e.g. send a `stop` to the server stdin, not a `kill -9`). # scaffold the `scaffold` block declares starter files kern writes into a fresh instance directory. each entry has a `path`, `content`, and an optional `when` condition. content supports `{{userOverrides.*}}` templating. ```jsonc "scaffold": { "env": { "path": ".env", "content": "PORT={{userOverrides.port}}\nLOG_LEVEL={{userOverrides.log_level}}\n" }, "readme": { "path": "README.md", "content": "Web API ({{userOverrides.port}})…" }, "entry": { "path": "app.js", "content": "…", "when": "{{userOverrides.runtime}} == 'node'" } } ``` ## when conditions `when` gates whether a file is written. this lets runtime-conditional scaffolding generate only the right starter files — the discord bot plugin uses this to emit package.json only for node/bun/deno runtimes, and a cargo manifest only for rust. > **note** scaffold runs once when the instance is created (or on explicit `install`). it does not clobber existing files on subsequent starts. # config schema `configSchema` is an array of fields. the **host renders this as a dynamic form** when creating or editing a server instance — plugins never build their own config ui for the basics. the resolved values become `userOverrides`, available to `lifecycle` and `scaffold` via `{{userOverrides.*}}`. ## field types ```jsonc "configSchema": [ { "key": "port", "label": "Port", "type": "text", "default": "3000" }, { "key": "log_level", "label": "Log Level", "type": "select", "options": ["debug","info","warn","error"], "default": "info" }, { "key": "max_connections", "label": "Max Connections", "type": "text", "default": "100" } ] ``` | field | type | notes | |---|---|---| | `key` | string | the userOverrides key. | | `label` | string | shown in the form. | | `type` | `"text"` \| `"select"` | the two supported types today. | | `default` | string | used on fresh instances. | | `options` | string[] | only for `select`. | ## dependsOn — cascading defaults fields may declare `dependsOn` to cascade defaults when another field changes. for example, a plugin can use this so that picking a runtime adjusts the suggested entry file and build command. > **note** keep config fields minimal. every field is one more decision a user has to make to get a server running. ship sensible `default`s. # plugin ui a plugin’s `dist/index.js` is an esm bundle that exports `mount(mountPoint, serverData, hostApi)`. kern mounts it inside an **isolated shadow dom** so plugin tailwind never bleeds into the host shell, and injects `dist/index.css` into that shadow root. ## mount ```ts export function mount( mountPoint: ShadowRoot, serverData: ServerData, hostApi: HostApi, ) { // render your ui into mountPoint } ``` ## the host api `hostApi` is the bridge to tauri and the shell: - `invoke(command, args)` — call tauri/rust commands. - `serverPath` — the instance directory. - `listen(event, cb)` — subscribe to tauri events. - **extension registrars:** - `registerTab({ id, label })` — add a tab to the server detail view. - `registerToolbarAction(...)` — add a toolbar button. - `registerSidebarItem(...)` — add a sidebar entry. plugins use `registerTab` to add custom tabs to the server detail view. ## isolation because your ui runs in a shadow root: - your tailwind/css only affects your own dom. - the host’s styles never leak in either (except via css custom properties the host explicitly exposes). - you can bundle whatever styling approach you want — plain css, tailwind, css-in-js. > **warn** don’t reach out of the shadow root to style the host. that breaks isolation and will break on updates. # packaging a `.kern` file is a **zip archive** with a specific layout. you build it from your plugin source. ## layout ``` my-plugin.kern (a zip) ├─ manifest.json (required) ├─ dist/ │ ├─ index.js (optional — the esm ui bundle) │ └─ index.css (optional — injected into the shadow root) └─ …any other assets your bundle references ``` ## building build your ui bundle to `dist/index.js` (esm, exporting `mount`), then zip the manifest + `dist/` (+ assets) into a `.kern` file. the kern app has a `create_plugin_package` helper that does the zipping for you against a validated manifest. ## dev seeding for local development you can drop a `.kern` into kern’s plugin directory (or double-click it to trigger `kern://install`) and the host will pick it up. this lets you iterate on a plugin without going through the registry. > **note** validate your `manifest.json` before zipping. the registry’s ci (phase a) unzips, parses the manifest, checks `id`/`version`/schema, and rejects malformed packages on the pr. # distribution the registry is live at [kern.aaenz.no](https://kern.aaenz.no) and free to use. there is no pr queue: sign in with github and publish directly. ## self-publish through the web 1. sign in at kern.aaenz.no with github. 2. **submit a plugin** — upload your `.kern`, fill in the details (display name, category, readme, screenshots), and the listing goes live with your first version. 3. **publish new versions** from the plugin’s edit page — drag the `.kern`, add a changelog, hit publish. the public page updates immediately. every version records its `sha256` and size, and kern verifies the hash against the package before installing. storage policies scope uploads to your own plugin path, so no one else can replace your files. plugins published by the official `ellipog` account get a `verified` badge. ## the cli publisher maintainers can publish a prebuilt bundle without the browser: ```bash npm run publish:plugins -- ../kern/release-assets/plugins ``` it verifies each file against the advertised sha256 + size, uploads to storage, and replaces the matching version rows. it needs `SUPABASE_SERVICE_ROLE_KEY` in the environment. ## the in-app marketplace kern lists the registry from inside the app (plugins → marketplace): browse, search, and install without leaving the app. the registry url is configurable in settings (`registryUrl`). ## the kern:// deep link the website’s "install in kern" buttons fire: ``` kern://install?url=&id=&v= ``` if kern is installed, it opens and installs. if not, the site falls back to "download kern first". > **danger** kern plugins run with full local privileges. only install plugins from authors you trust. read the readme, check the author’s github, and mind the install counts before installing. # plugin security plugins are how kern learns new server types, which means third-party code runs inside the app. the model is honest about what it is: a **capability boundary**, not a sandbox. ## the boundary - plugin ui runs in a shadow root for **style** isolation, but shares the host webview realm — it is not a js sandbox. - every call to the host goes through `HostAPI.invoke`, which checks the plugin's manifest permissions against a command → permission map. a command whose permission is absent **fails closed** — it isn't callable at all. - unknown permission names in a manifest are rejected at install time; there is no way to smuggle a capability in. ## permission catalogue | permission | grants | |---|---| | `servers:read` | read the server list and configuration | | `servers:write` | create, edit, and delete servers; app settings | | `files:read` | read files inside server directories | | `files:write` | write, rename, delete files inside server directories | | `process` | start/stop/control processes; run commands | | `downloads` | download files and java runtimes | | `backups` | create, restore, delete world backups | | `metrics` | read cpu / ram / network metrics | | `plugins:manage` | install and remove other plugins | | `plugin:kv` | store plugin state in its private data store | | `plugin:secrets` | store/read secrets in the os credential vault | | `rcon` | connect to the server's rcon console | | `sync` | export/import configuration to git | | `ui` | read and write ui state | declare only what you use. the install dialog shows the human-readable list — a plugin asking for `process` + `files:write` to "display weather" is a red flag you can see before installing. ## install consent & integrity 1. the `.kern` archive is inspected (`manifest.json` validated, path traversal rejected — zip-slip would otherwise be remote code execution). 2. required permissions are shown; installation proceeds only on explicit consent. 3. a sha256 checksum of the package is recorded. registry installs verify the checksum of the downloaded artifact; a mismatch aborts the install. there is **no publisher signing** — the trust model is "consent + checksum", not verified identity. treat community plugins like browser extensions: install ones you trust. ## what a malicious plugin could do honestly: with `process` and `files:write`, a plugin can run code as you. the boundary limits *silent* capability escalation (no undeclared filesystem walks, no plugin-manager calls it didn't ask for), and it makes capabilities visible at install time. it does not contain a determined attacker. if that matters for your threat model, audit the plugin's bundle before installing. ## writing safe plugins - request the narrowest permissions that work; `plugin:kv` for state, `plugin:secrets` for tokens (never hardcode). - validate `serverData` paths before use — the host gives you the instance path, not a promise the file exists. - don't reach out of your shadow root to style or mutate the host. - keep the install step idempotent and non-destructive. # kern-cli `kern-cli` controls a **running** kern app from the terminal or a script. it talks to the loopback [automation api](/docs/automation-api) on `127.0.0.1` with a bearer token the app publishes for it — you never configure an address. it installs alongside the app on windows (`%localappdata%\kern\kern-cli.exe`) and ships as a release asset (`kern-cli`, `kern-cli.exe`) on every platform. ```bash kern-cli status kern-cli list kern-cli start "My Server" --wait kern-cli logs "My Server" --follow --grep ERROR ``` run bare `kern-cli` on a terminal for the **dashboard** (below). `kern-cli help` lists everything; every command has `--help`. ## commands ### observe | command | what it does | |---|---| | `status` | app version, api version, host cpu/ram, running count | | `list` | fleet table: status, cpu, ram, uptime, group | | `show ` | one instance in detail, including ports and last crash | | `top` | live fleet view (`--once` for a single snapshot) | | `host` | host-wide cpu/ram | | `metrics ` | metric history (`--spark` for sparklines, `--window 86400`) | | `energy ` | estimated running cost from your power price | | `port ` | listening ports + quick-connect strings | | `events` | audit feed with status transitions (`--follow` to stream) | | `audit` | the audit log (`--limit`, `--server`) | ### control | command | what it does | |---|---| | `start ` | start one or many | | `stop ` | graceful stop (stdin → timeout → force-kill) | | `restart ` | stop then start | | `install ` | run the plugin's install lifecycle step | | `wait --for running\|stopped\|healthy` | block until a state | | `send ` | write to the server's stdin (`say` is an alias) | `start/stop/restart/install` accept fleet selectors instead of names: ```bash kern-cli stop --tag prod --wait --timeout 2m kern-cli restart --group minecraft kern-cli start --all ``` `--wait` blocks until the action reaches its target state. `healthy` means running, clear of a fault status, and below 95% cpu/ram. ### logs ```bash kern-cli logs "My Server" --lines 200 kern-cli logs "My Server" --follow kern-cli logs "My Server" --follow --grep "OutOfMemory|FATAL" --exclude "at java" kern-cli logs "My Server" --format json # ndjson when following ``` `--follow` is offset-based: it streams only new lines, survives log rotation, and never re-reads the tail. `--grep` / `--exclude` take rust regex syntax. ### backups and tasks ```bash kern-cli backup list "My Server" kern-cli backup create "My Server" --wait kern-cli backup restore "My Server" world-2026-09-12.zip --yes kern-cli backup delete "My Server" old-backup.zip --yes kern-cli task list "My Server" kern-cli task run "My Server" nightly-restart ``` restore and delete require `--yes`; both are destructive. ### registry ```bash kern-cli add ./my-api --name "Prod API" --type custom --group prod --tag live kern-cli add ./paper-server --import # adopt an existing folder kern-cli inspect ./paper-server # see what import would detect kern-cli edit "Prod API" --group staging --tag blue --auto-start kern-cli rm "Prod API" kern-cli rm "Prod API" --folder --yes # also delete the directory ``` ### plugins ```bash kern-cli plugin list kern-cli plugin validate ./my-plugin.kern kern-cli plugin install ./my-plugin.kern kern-cli plugin remove discord_bot ``` ### diagnostics ```bash kern-cli doctor # endpoint file, connectivity, version alignment kern-cli endpoint # print the api url (--show-token, --format json) kern-cli api GET /servers # raw request against any endpoint ``` ## global flags | flag | effect | |---|---| | `--format table\|plain\|json` | table (default), tab-separated rows, or raw JSON | | `--color auto\|always\|never` | ansi colors; `NO_COLOR` is respected | | `-q, --quiet` | suppress informational output | | `--version` | cli version | | `-h, --help` | help for any command | `--format json` prints the raw api response, so the cli doubles as a JSON client. `--format plain` is one record per line — friendly to `awk`/`cut`. ## exit codes | code | meaning | |---|---| | `0` | success | | `1` | runtime or api error | | `2` | usage error (bad flags, unknown command) | | `3` | server / plugin / backup / task not found | | `4` | app unreachable (not running, automation off, stale token) | | `5` | `--wait` timeout | ```bash kern-cli wait "My Server" --for healthy --timeout 90s && echo "shipping" ``` ## naming targets resolve in this order: exact id → exact name (case-insensitive) → unique id/name prefix → unique substring. an ambiguous match lists the candidates and exits `3`, so `kern-cli stop api` can never quietly stop the wrong server when both `api-prod` and `api-staging` exist. ## environment | variable | effect | |---|---| | `KERN_APP_DATA_DIR` | override the app data directory (endpoint discovery) | | `KERN_AUTOMATION_URL` + `KERN_AUTOMATION_TOKEN` | talk to a different endpoint (tunnel / CI) | | `NO_COLOR` | disable ansi colors | ## the dashboard bare `kern-cli` (or `kern-cli dash`) opens a full-screen dashboard: fleet table, live log tail for the selected server, and an event ticker. it polls the same api, so it works over `KERN_AUTOMATION_URL` too. ``` q quit · ↑↓ select · s start · x stop · r restart · b backup · i install · / filter · : command · ? help · PgUp/PgDn logs ``` | key | action | |---|---| | `↑` `↓` / `j` `k` | select a server | | `s` `x` `r` `i` | start / stop (confirm) / restart (confirm) / install | | `b` | back up the selected server | | `PgUp` `PgDn` | scroll the log pane | | `Home` `End` | log top / follow | | `/` | filter the fleet | | `:` | command bar — `start|stop|restart `, `backup `, `filter `, `clear`, `quit` | | `?` | help | | `q` / `ctrl+c` | quit | ## shell completions ```bash kern-cli completions bash > ~/.local/share/bash-completion/completions/kern-cli kern-cli completions zsh > "${fpath[1]}/_kern-cli" kern-cli completions fish > ~/.config/fish/completions/kern-cli.fish kern-cli completions powershell | Out-String | Invoke-Expression ``` > **note** create an alias when the hyphen gets old: `alias kern=kern-cli`. ## common workflows **deploy then restart, from a script** ```bash kern-cli wait "Prod API" --for stopped --timeout 1m \ && rsync -a ./build/ server:/srv/api/ \ && kern-cli start "Prod API" --wait --timeout 2m ``` **watch for OOM across the fleet** ```bash kern-cli logs "Minecraft" --follow --grep "OutOfMemoryError" | while read -r line; do echo "[oom] $line" done ``` **morning fleet check** ```bash kern-cli list --format plain | awk -F'\t' '$4 != "true" { print "down:", $2 }' ``` see [recipes](/docs/recipes) for more. # web remote the web remote is a full control panel served by kern itself: console, a Monaco file editor, backups, schedules, metrics, audit, plugin installs — the same actions the desktop app performs, reachable from a phone, a laptop, or anywhere through a tunnel. enable it under **settings → web remote**. ``` https://192.168.1.20:7440 ``` ## pairing devices pair with **single-use invites**. the owner creates one under **settings → web remote people** (name, role, optional server list), then shares the QR or link: ``` https:///#invite=ABCD2345 ``` the invite expires in 15 minutes, works once, and redeems into a named device token stored on that device only. the owner's original keyring token keeps working as an admin credential (the legacy `?token=` QR), so existing setups don't break. - **viewer** — read only: status, logs, metrics, files, audit. - **operator** — viewer plus lifecycle, console input, file writes, backups, tasks. - **admin** — everything, including creating/removing instances, installing plugins, and managing people. roles can additionally be scoped to specific server ids. everything a remote user does is written to the audit log with their name. ## where it listens by default the panel binds every interface on port `7440`. **settings → web remote → bind address** narrows that: - **all interfaces (0.0.0.0)** — phones on your lan can reach it. - **localhost only (127.0.0.1)** — nothing on the lan can connect; use the tunnel, or put your own reverse proxy (nginx/caddy) in front. the proxy should forward to `https://127.0.0.1:7440` and may skip origin verification (self-signed cert). - **any detected interface ip** — bind to one specific address (a second nic, a vpn interface). the panel shows every URL it is reachable at. kern serves a self-signed certificate covering localhost and your interface addresses; when you bind a new address kern regenerates it automatically, and **regenerate** in settings forces a fresh one (devices re-accept once). through a tunnel, cloudflare serves a real certificate instead — which is also what lets the PWA install. changing the bind or port restarts the listener immediately. binding is deliberately desktop-only: a wrong remote bind would strand every paired device. ## the panel - **overview** — host load, instance cards with live cpu/ram, start/stop/restart, and (admins) a **new instance** flow: point at a folder, kern inspects it, you pick a plugin and fill its config form. - **console** — live stream (server-sent events), command input to stdin, history, filter, autoscroll, log download, saved command snippets, and an RCON player list. - **files** — the desktop's own Monaco editor (same theme, format-on-save), lazy file tree, tabs with unsaved indicators, create/rename/delete/upload/download, markdown/json/image previews, cross-file content search with jump-to-line, per-file **snapshot history** (capture, restore, delete) and diff against any snapshot. saves are conflict-checked against the file on disk. - **backups** — create/restore/delete/download, plus the automatic-backup schedule (interval, retention, on clean stop). - **tasks** — full scheduler editor: daily / interval / cron / manual, command and restart actions, enable toggles, run-now. - **metrics** — cpu/ram charts from the rolling 7-day history. - **plugins** (admins) — installed plugins with uninstall, `.kern` upload-install, and the registry marketplace with install progress. - **audit** — the full action history, including remote requests, with an admin download. - **settings** — device info, bind/tunnel status, crash notifications, and (admins) invite/user/device management. ## public access via cloudflare tunnel two modes, both powered by a `cloudflared` child process that dials out — no port forwarding: - **quick** — no account needed. kern runs `cloudflared tunnel --url https://localhost:` and shows the random `*.trycloudflare.com` URL. rate-limited and best for personal, occasional access. - **named** — stable hostname on your own domain. create a tunnel in the [Cloudflare Zero Trust dashboard](https://one.dash.cloudflare.com/), route a public hostname to `https://localhost:7440`, then paste the connector token under **settings → web remote → named**. the token lives in the os credential vault (never `config.json`). the connector dials the address the panel is actually bound to, both modes restart automatically if cloudflared exits while the toggle is on, and the settings panel shows the current URL, the pairing QR, and the last connector error. if `cloudflared` isn't installed, kern offers to download the official binary into its app data (the pairing panel has the progress). ### protecting the tunnel with cloudflare access a public URL plus a valid device token is full control, so put access in front for anything long-lived: 1. in Zero Trust → **Access → Applications**, add a self-hosted app for your hostname. 2. add a policy (one-time pin to your email, google, github — whatever you use). 3. leave the kern device tokens in place: access gates the edge, kern gates the panel. both must pass. > **warn** quick tunnel URLs are public by design and rotate whenever the tunnel restarts. anyone with the URL *and* a paired device token controls your servers. keep the tunnel off when you don't need it, revoke devices you no longer trust, and prefer named tunnels + access for daily use. ## notifications the panel can notify you when an instance transitions into a fault (crash, forced stop, error). enable it under **settings → crash notifications** — the browser asks for permission once. notifications fire while the panel is open (including as an installed PWA in the background on most platforms). ## security model - **https everywhere.** on the lan kern serves a self-signed certificate (accept it once per device). through a tunnel, cloudflare terminates tls with a real certificate. - **device tokens, not one shared secret.** tokens are random, stored hashed on the host, revocable per device, and carry a role + server scope. the plaintext only ever exists on the paired device. - **invites are single-use.** 15-minute ttl, strict rate limiting on pairing and auth failures, and every remote mutation is attributed in the audit log. - **scoped api.** the browser talks to the same api the desktop app and CLI use, behind a per-route scope policy (view / control / admin). unknown routes require admin — new endpoints can't leak to viewers by accident. ## firewall the first time the remote binds to the lan, windows firewall shows its standard "allow access" prompt. allow **private networks** only; access is token-authenticated either way. this is an os dialog, not a kern one. > **note** binding to `127.0.0.1` avoids the firewall prompt entirely (nothing on the lan can reach it). ## turning it off **settings → web remote → disable** stops the listener immediately; paired devices keep their tokens so re-enabling doesn't require re-pairing. revoke individual devices under **web remote people**, or rotate the owner token to invalidate the legacy credential. ## scripts for automation on the same machine, use the [automation api](/docs/automation-api) instead — loopback http, no certificate dance, same routes. `kern-cli` speaks it natively. # backups kern snapshots an instance's `world/` directory into a zip under `/backups/`: ``` / ├─ world/ └─ backups/ ├─ world-2026-09-12T04-00-02.zip ├─ world-2026-09-11T04-00-01.zip └─ pre-restore-1789237801.zip ``` names are timestamped, so listing is sorting. a restore always snapshots the current world as `pre-restore-.zip` before touching anything. ## manual snapshots **ui** — the monitor tab has **snapshot now**. **cli** — `kern-cli backup create "My Server" --wait`. **api** — `POST /servers/{id}/backup` (answers `202` and runs in the background). kern refuses a backup that wouldn't fit on disk rather than filling the drive half-way through. ## schedules each instance has a backup schedule: | field | meaning | |---|---| | `intervalSecs` | snapshot every n seconds. `0` disables the interval. `7200` = every 2h | | `keep` | rolling retention — oldest archives beyond this count are pruned | | `onStop` | also snapshot whenever the instance stops cleanly | | `lastBackupSecs` | host-managed; the scheduler writes it after each run | set it in the instance's **monitor → backups** panel or via `PATCH`-ing the config. the scheduler runs on the same 30-second worker as alerts and task runs, so an interval is approximate to within a tick. > **note** retention pruning runs after every snapshot. if you snapshot into a folder that also holds one-off archives, keep `keep` generous. ## restore **cli** — `kern-cli backup restore "My Server" world-2026-09-12T04-00-02.zip --yes` **api** — `POST /servers/{id}/backups/{name}/restore` the restore: 1. zips the current `world/` to a `pre-restore` archive (your undo), 2. deletes `world/`, 3. extracts the chosen archive into a fresh `world/`. archive entries are path-checked; an entry pointing outside `world/` aborts the restore. > **warn** stop the instance first. restore replaces files on disk while a running server may still be writing them — `kern-cli stop "My Server" --wait` then restore, then `start`. ## deleting **cli** — `kern-cli backup delete "My Server" old.zip --yes` deletion is immediate and cannot be undone; the `pre-restore` archives are ordinary backups and can be deleted the same way (do it knowingly — that's your undo). ## what isn't backed up only `world/`. plugins, configs, and jars are code — keep those in git. a backup restore won't resurrect `server.properties` or plugin data. ## automation recipes nightly backup + restart, with the backup running before the restart: ``` 04:00 daily → backup → broadcast "restarting" → stop → start ``` see [tasks](/docs/tasks) for the schedule grammar and [recipes](/docs/recipes) for webhook notifications on backup failure. # scheduled tasks every instance can carry its own scheduled tasks. a task fires when **any** of its configured schedule modes is due, and never twice in the same minute. | field | meaning | |---|---| | `id` | stable id (host-generated) | | `name` | label shown in the ui | | `enabled` | off keeps the task but stops firing | | `action` | `restart` \| `start` \| `stop` \| `command` \| `backup` \| `health` | | `command` | payload — see below | | `intervalSecs` | run every n seconds (`0` = unused) | | `dailyAt` | local `HH:MM` (`""` = unused) | | `cron` | 5-field cron: `min hour dom month dow` | | `announceMinutes` | for restarts: minutes before the restart to warn players in the console | | `lastRunSecs` | host-managed dedupe | ## actions | action | `command` holds | effect | |---|---|---| | `restart` | `""` | graceful stop → start, with announcements | | `start` / `stop` | `""` | lifecycle | | `command` | the line | sent to stdin when running; run through the shell when stopped | | `backup` | `""` | snapshot `world/` and prune to the retention count | | `health` | `notify` or `restart` | checks the instance each tick; `restart` recovers a dead process | ## schedule examples ``` every 6h intervalSecs = 21600 daily at 04:00 dailyAt = "04:00" weekdays at 04:00 cron = "0 4 * * 1-5" mondays at 03:30 cron = "30 3 * * 1" ``` restart tasks can announce a countdown: ```json { "name": "nightly-restart", "action": "restart", "dailyAt": "04:00", "announceMinutes": [5, 1] } ``` that sends the manifest's pre-restart console line at t-5m and t-1m (for minecraft: `say restarting in 5 minutes`) before stopping. ## running tasks - **ui** — monitor → tasks has **run now** per task. - **cli** — `kern-cli task list "My Server"`, `kern-cli task run "My Server" nightly-restart` (name or id). - **api** — `POST /servers/{id}/tasks/{taskId}/run`. every run lands in the [audit log](/docs/automation-api) (`task` action) and raises a notification. ## notifications task failures and backup results use the normal notification path — in-app center, native toast when unfocused, and your [webhook](/docs/notifications) if configured. a webhook is the usual way to learn that a 4am restart didn't come back up. > **note** a task's schedule lives in `config.json`; edits through the ui or `PATCH /servers/{id}` take effect on the next scheduler tick (up to 30s). # monitoring & alerts ## live telemetry kern samples **process-tree** cpu and ram — a server's children count toward it (node workers, `cargo`/`rustc`, a jar's threads). the detail header draws this as the reactor channel: cpu shimmers along the top row, ram fills from the left, both turn amber past 90% and red on fault. the fleet dashboard shows the same numbers for every instance at once. first sample after a start reads ~0% — `sysinfo` reports cpu as a delta between reads, so the spin-up settles over the first second. ```bash kern-cli host # host cpu/ram kern-cli top # live fleet view kern-cli metrics "My Server" --spark ``` ## history a background worker records one sample per instance every 30 seconds into a rolling in-memory ring (about 7 days). the monitor tab graphs 24h / 7d cpu and ram. history is not persisted across restarts — it's telemetry, not a database. ```bash kern-cli metrics "My Server" --window 86400 --spark ``` ## health alerts per-instance rules fire when a metric stays above a threshold for a sustained window: | field | meaning | |---|---| | `cpuThreshold` | cpu fraction (`0.9` = 90%). `null` disables | | `ramThreshold` | ram fraction of the whole machine | | `sustainedSecs` | how long the threshold must hold before firing | configure under **monitor → alerts**. a fired alert goes to the [notification center](/docs/notifications) (and your webhook) once, then re-arms after the value recovers. the alert state is also visible in the tray icon: amber sweep/blips and a tooltip marker. ## crash reports when a process exits unexpectedly, kern writes the exit code plus the last log lines to `/crashes/.json` and shows a **last crash** card on the monitor tab. ```bash kern-cli crash "My Server" ``` if the [watchdog](/docs/tasks) is enabled, the restart attempt is recorded too, and a crash-loop backs off exponentially instead of thrashing. ## listening ports kern matches the instance's process tree against the os socket table and surfaces the ports it actually bound, with a copy-ready connect string. ```bash kern-cli port "Minecraft" # :25565 → localhost:25565 ``` preflight uses the last-observed ports to warn before a start: ```bash kern-cli preflight "Minecraft" # ! port 25565 held by javaw.exe (pid 8123) # ! minecraft eula is pending (edit eula.txt) ``` ## energy & cost with a power price and machine wattage set in settings, kern estimates each instance's running cost (draw scales between an idle baseline and full load): ```bash kern-cli energy "My Server" ``` > **note** it's an estimate for "what does this habit cost me", not a meter. plug a real meter into the wall if the number matters. # notifications, webhooks & alerts every event kern surfaces — crashes and watchdog restarts, health alerts, backup results, schedule runs, update checks — lands in the **notification center** (the bell in the title bar, with jump-to-server). when the window isn't focused, the same notification is mirrored to a **native os toast**. ## do not disturb settings → notifications & alerts → *native os notifications* removes the os mirror while keeping the in-app center. the setting is stored in `config.json` and survives restarts. ## webhooks set a **webhook url** and enable *send webhook events*. every notification is posted as json: ```json { "content": "[error] Server crashed\nexit 1", "text": "[error] Server crashed\nexit 1" } ``` `content` is what discord reads; `text` is what slack incoming webhooks read. a generic consumer gets both. delivery is best-effort with a 10-second timeout — a dead endpoint never blocks a lifecycle action. ## log alerts rules are regular expressions matched against every streamed log line. a match raises a notification (and fires the webhook), throttled to once per minute per rule so a repeating error can't spam you. | rule | pattern | | --- | --- | | out of memory | `OutOfMemoryError` | | server lag | `(?i)can't keep up` | | server errors | `\[Server thread/ERROR\]` | patterns use rust regex syntax. invalid patterns are skipped with a note in the app log rather than disabling the other rules. manage rules under settings → notifications & alerts. # keyboard shortcuts `mod` is `ctrl` on windows/linux and `cmd` on macos. ## global | keys | action | |---|---| | `mod` `k` | command palette — jump to an instance, run start/stop/restart, open settings | | `esc` | close the palette / dialog / search | ## server detail | keys | action | |---|---| | `enter` | send the console input line | | `↑` / `↓` | cycle the local command history in the console input | | `esc` | close the find/replace overlay or a dialog | ## file editor | keys | action | |---|---| | `mod` `f` | toggle the editor search panel (when the editor isn't focused) | | `mod` `s` | save the active file | | `alt` `shift` `f` | format the active file (monaco's formatter, where available) | | `esc` | close the search panel | ## dialogs `esc` cancels. `enter` confirms the focused button. destructive dialogs (`stop`, delete, restore) require an explicit click or the confirm key — nothing triggers on a stray keypress. ## cli dashboard bare `kern-cli` opens a full-screen dashboard with its own keys: | keys | action | |---|---| | `q` / `ctrl+c` | quit | | `↑` `↓` / `j` `k` | select a server | | `s` `x` `r` `i` | start / stop / restart / install | | `b` | back up the selected server | | `PgUp` `PgDn` | scroll the log pane | | `Home` / `End` | log top / follow | | `/` | filter the fleet | | `:` | command bar (`start all`, `restart minecraft`, `backup `, …) | | `?` | help | ## docs search | keys | action | |---|---| | `mod` `k` | focus the docs search on this site | | `↑` `↓` | move through results | | `enter` | open the highlighted result | | `esc` | clear the query | > **note** shortcuts are contextual on purpose — `mod+k` works everywhere, but console keys only fire while the console input is focused, and editor keys only inside the editor. # troubleshooting start with `kern-cli doctor` — it checks the endpoint file, connectivity, api version, and registry in one shot. ## install & first launch **windows: "windows protected your pc"** — smart screen, because the installer isn't os-code-signed. *more info → run anyway*. updates afterwards are minisign-signed and verified before install. **macos: "kern can't be opened"** — gatekeeper, same reason. right-click the app → *open* once. no intel build yet; apple silicon only. **linux: appimage won't launch** — `chmod +x kern_*.AppImage` then run it. fuse is needed: install `libfuse2` on debian/ubuntu if the appimage complains. ## cli says "could not read …automation.json" the cli found no running app: 1. is kern running? `kern-cli doctor` names the path it looked in. 2. is the automation api enabled? **settings → automation & cli** — it's on by default (port `7442`). 3. are you in the same user session? the app data dir is per-user; `sudo kern-cli` won't find your config. 4. using a portable/dev setup? point `KERN_APP_DATA_DIR` at the directory that holds `automation.json`. ## cli exits 4 but kern is running usually a stale endpoint file from a crashed instance, or a port clash: - `kern-cli doctor` shows the endpoint's `pid` and start time. compare with the actual process. - `kern-cli endpoint --format json` prints the url the cli is using. - restart kern — it rewrites `automation.json` (keeping the token). - if two kern instances run on one machine, only the first binds `7442`; the second logs `[automation] failed to bind`. give one a different `automationPort`. ## cli exits 4 with 401 the token in the endpoint file doesn't match the running app. restart the app, or delete `automation.json` and restart to re-mint. if you use `KERN_AUTOMATION_TOKEN`, check for a stale value in your shell profile. ## a server won't start run `kern-cli preflight ""`. the usual suspects: | finding | fix | |---|---| | port held by another process | stop the other process, or change the instance's port config | | minecraft eula pending | set `eula=true` in `eula.txt` in the instance folder | | low disk | free space; backups and logs need room | | `custom instances require a start_command` | it's a `custom` instance with no command — edit the instance and set one, or use the right plugin | | `java not found` | install a jre/jdk (17+ for modern minecraft) or point the instance at a java path in its config | ## stop takes 30 seconds and reports "stopped-forced" that's the graceful window expiring. kern sent the stop command (stdin or the plugin's `stop` step), waited, then force-killed the process tree. if it's always forced: - the server ignores its stop command — check the instance's `stopCommand`. - the world save genuinely takes that long — raise `stopTimeoutSecs` on the instance. - it's a custom instance with `stopCommand: ""` — the stdin step is deliberately skipped; kern waits the full timeout then kills. set a stop command if the process supports one. ## "orphaned" instance the folder is missing (moved, renamed, external drive unplugged). kern keeps the record instead of deleting it. re-point the instance at the folder via **edit**, or remove the record. nothing is deleted automatically, ever. ## web remote unreachable from my phone 1. same network? guest wi-fi often isolates clients. 2. the app binds `0.0.0.0:` only while enabled — check settings. 3. windows firewall: allow the private-network prompt; if you dismissed it, add an inbound rule for the port. 4. https warning is expected (self-signed cert) — accept it once. 5. if you changed `webRemotePort`, re-scan the qr (the url embeds the port). ## logs pane is empty - the log file is `/latest.log`; a server that has never produced output has none. - kern appends stdout/stderr to it while running, so a stopped-and-cleared folder reads empty. - `kern-cli logs "" --lines 20` shows the tail without the ui. ## plugin install rejected the dialog names the reason. common ones: - **unknown permission** — the manifest requests a permission this kern version doesn't know; update kern or the plugin. - **kernCompat** — the plugin requires a newer host version. - **checksum mismatch** — the downloaded `.kern` changed in transit; re-download. - **path traversal** — the archive contains entries that escape the plugin directory; refuse it and tell the author. see [plugin security](/docs/plugin-security). ## high cpu reading right after start `sysinfo` computes process cpu as a delta between samples. the first reading after a launch is meaningless (often 100% for host, ~0% for the new process) and corrects within a second or two. it is not your server. ## the dashboard looks broken in my terminal the full-screen dashboard needs a real terminal. if it garbles, check `TERM`, try windows terminal, or use the plain commands (`kern-cli top --once`, `kern-cli list`) which never use the alternate screen. ## in-app updater says "signature mismatch" the downloaded artifact didn't verify against the embedded public key. this is the updater protecting you. retry; if it persists, download the installer fresh from the [releases page](https://github.com/aaen-studios/kern/releases). **never** disable signature checks. ## where do i report a bug [github issues](https://github.com/aaen-studios/kern/issues). include `kern-cli doctor` output, your os, and the exact command or click path. # recipes small, complete setups built from kern's own knobs. each one is just config + a task or two. ## nightly backup before restart the safe restart: snapshot, warn players, restart, and only then prune. 1. instance → **monitor → backups**: `intervalSecs: 0`, `onStop: true`, `keep: 14`. 2. instance → **tasks** → new task: - action `backup`, `dailyAt "03:55"` - action `restart`, `dailyAt "04:00"`, `announceMinutes [5, 1]` the 03:55 backup runs while the server is still up; the restart's `onStop` snapshot catches anything written in those five minutes. ## discord ping when a server dies 1. settings → notifications → **webhook url**: paste your discord webhook. 2. enable **send webhook events**. 3. instance → **monitor → watchdog**: enable auto-restart (max 5 attempts). crash, restart attempts, and give-up events all hit the webhook. every payload carries both `content` (discord) and `text` (slack). ## alert on OOM before it kills the server 1. instance → **monitor → alerts**: cpu threshold `0.9`, sustained `60s`. 2. settings → notifications → add a log alert rule: pattern `OutOfMemoryError|java\.lang\.OutOfMemory`, enabled. log alerts fire from the streamed console the moment the line appears — usually minutes before the process actually dies. ## deploy and restart, from ci ```bash #!/usr/bin/env bash set -euo pipefail kern-cli stop "Prod API" --wait --timeout 60s rsync -a --delete ./build/ deploy@host:/srv/api/ kern-cli start "Prod API" --wait --timeout 120s kern-cli wait "Prod API" --for healthy --timeout 60s echo "deployed $(date -u +%FT%TZ)" ``` exit codes make failures loud: `3` if the instance was renamed, `4` if kern isn't running, `5` if it never came healthy. ## morning fleet check ```bash kern-cli list --format plain | awk -F'\t' '$4 != "true" { print "down:", $2 }' kern-cli list --format plain | awk -F'\t' '$4 == "true" { n++ } END { print n " up" }' ``` ## stop everything when you leave the house one task per instance — or a single fleet command from a shortcut / phone automation: ```bash kern-cli stop --tag minecraft --wait --timeout 2m ``` ## crash loop breaker watchdog with a low attempt count turns a broken plugin into five attempts and a notification instead of an all-night restart loop: - **monitor → watchdog**: attempts `3`. - **monitor → alerts**: ram threshold `0.85`, sustained `120s` — memory creep usually precedes the crash. `kern-cli crash "Minecraft"` then shows the exit code and the last lines, no log spelunking. ## import an existing paper server ```bash kern-cli inspect ./paper-1.21 # jars, world, eula, suggested runtime kern-cli add ./paper-1.21 --import --name "Paper" --group minecraft kern-cli start "Paper" --wait ``` `--import` adopts the folder as-is — kern never moves or rewrites files it didn't create. ## stream logs into your own alerting ```bash kern-cli logs "Minecraft" --follow --grep "ERROR|FATAL" --format json \ | jq -r '.line' \ | while read -r line; do curl -s -X POST "$ALERT_URL" -d "$line"; done ``` offset-based following means no duplicate lines, even across rotations. # ai agents these docs are packaged as an **agent skill** — a folder of markdown an AI coding agent loads when the task touches kern. instead of scraping the site, your agent gets the manifest reference, lifecycle rules, kern-cli surface, and automation api as local files. the skill lives in [skills/kern](https://github.com/ellipog/kern-web/tree/main/skills/kern) and its `references/` directory is generated from these same docs, so the two can't drift. ## install with the skills cli (claude code, opencode, cursor, and ~70 more agents): ```bash npx skills add ellipog/kern-web ``` or as an npm package (pinned, offline): ```bash npm i -D @aaen-studios/kern # the skill is at node_modules/@aaen-studios/kern/kern ``` or copy it into whichever directory your agent scans: ```bash git clone --depth 1 https://github.com/ellipog/kern-web cp -r kern-web/skills/kern .claude/skills/kern # or .agents/skills/kern, .cursor/skills/kern, … ``` ## paste this into your agent don't want to install anything yourself? paste a one-liner and let the agent do it: ```text install the kern agent skill by running: npx skills add ellipog/kern-web — then use that skill to help me build a .kern plugin. ``` ## no install at all agents that can fetch a url don't need the skill installed: - [llms.txt](https://kern.aaenz.no/llms.txt) — an index of every doc for ai tooling - [llms-full.txt](https://kern.aaenz.no/llms-full.txt) — the whole documentation in one response - `/raw/docs/` — the author-written markdown, frontmatter included, e.g. `/raw/docs/manifest-reference` ## what the agent gets - a `SKILL.md` router explaining when kern applies and which reference to read - `references/manifest-reference` — the `.kern` package and `manifest.json` schema - `references/lifecycle`, `references/config-schema`, `references/scaffold`, `references/plugin-ui` — plugin authoring - `references/cli` and `references/automation-api` — controlling a running app - plus every other page in these docs > **note** the skill is a documentation bundle, not a tool. installing it grants your agent no new access — it just knows what to write. ## keeping it current the npm package is versioned separately from the app; `npx skills update kern` (or reinstalling) picks up new docs. contributing? after editing any page here, run `npm run skill:build` so the bundled references regenerate, and `npm run skill:check` will fail the build if they're stale. # automation api kern exposes a plain-http JSON api bound to **`127.0.0.1` only** (never the lan). `kern-cli` is a client of this api; anything else — scripts, editors, ci — can be too. enable it under **settings → automation & cli**. the endpoint and bearer token are published to `automation.json` in the app data directory: ```json { "version": 2, "port": 7442, "token": "…64 hex chars…", "pid": 12345, "started_at": 1789237726 } ``` | platform | app data directory | |---|---| | windows | `%APPDATA%\com.ellio.kern` | | macos | `~/Library/Application Support/com.ellio.kern` | | linux | `$XDG_DATA_HOME/com.ellio.kern` (or `~/.local/share/com.ellio.kern`) | every request needs `Authorization: Bearer `: ```bash TOKEN=$(jq -r .token "$APPDATA/com.ellio.kern/automation.json") curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7442/status ``` ```powershell $ep = Get-Content "$env:APPDATA\com.ellio.kern\automation.json" | ConvertFrom-Json Invoke-RestMethod -Uri "http://127.0.0.1:$($ep.port)/status" -Headers @{ Authorization = "Bearer $($ep.token)" } ``` > **note** `GET /status` reports `apiVersion`. this docs page describes **api v2** (kern v0.3.0+). v1 clients (`/status`, `/servers`, `/servers/{id}/log`, lifecycle, stdin) keep working — v2 only adds endpoints and fields. ## conventions - all requests and responses are JSON (`content-type: application/json`). - errors are `{ "error": "message" }` with a `4xx`/`5xx` status. - `start`/`install` answer `200` once the action is underway. `stop`/`restart`/`backup`/`restore` answer `202 accepted` and finish in the background — poll the resource to observe completion. - path segments are percent-encoded (`backups/world%202026.zip/restore`). - request bodies are capped at 64 KiB; log reads at 2 MiB. ## endpoints ### app | method | path | returns | |---|---|---| | `GET` | `/status` (alias `/health`) | `{ status, version, apiVersion, pid, host: { cpu, ram } }` | | `GET` | `/host/metrics` | `{ cpu, ram, status: "host" }` | | `GET` | `/audit?limit=100&since=` | `{ entries: [...], now }` | | `GET` | `/events?since=&wait=30` | `{ entries, statuses: { id: status }, now }` | ### servers | method | path | notes | |---|---|---| | `GET` | `/servers` | list; add `?ports=1` for a live port scan per running instance | | `POST` | `/servers` | create; body `{ name, serverType, path, group?, tags?, autoStart?, imported?, userOverrides? }` → `201` | | `GET` | `/servers/{id}` | detail: config + `pid`, `uptimeSecs`, `metrics`, `ports`, `lastCrash` | | `PATCH` | `/servers/{id}` | sparse update: `name`, `group` (`null` clears), `tags`, `autoStart`, `stopCommand`, `stopTimeoutSecs`, `userOverrides` | | `DELETE` | `/servers/{id}?folder=1` | remove the record; `folder=1` also deletes the working directory | | `POST` | `/servers/{id}/start` | `200 { action: "started" }` | | `POST` | `/servers/{id}/stop` | `202` — graceful stdin → timeout → force-kill | | `POST` | `/servers/{id}/restart` | `202` | | `POST` | `/servers/{id}/install` | `200` — runs the plugin's install step | | `POST` | `/servers/{id}/stdin` | body `{ "line": "say hi" }` (raw text also accepted) | | `GET` | `/servers/{id}/log?lines=200&offset=` | `{ lines, nextOffset, size, reset, running }` | | `GET` | `/servers/{id}/metrics?window=3600` | `{ windowSecs, samples: [{ at, cpu, ram }] }` | | `GET` | `/servers/{id}/energy` | `{ id, hours, estWatts, cost, currencyNote }` | | `GET` | `/servers/{id}/preflight` | `{ conflicts: [{ port, pid, process }], eulaPending, lowDisk, freeMb }` | | `GET` | `/servers/{id}/crash` | `{ crash: null \| { at, exitCode, forced, tail } }` | | `GET` | `/servers/{id}/tasks` | `{ tasks: [...] }` | | `POST` | `/servers/{id}/tasks/{taskId}/run` | `{ ok: true }` | | `GET` | `/servers/{id}/backups` | `{ backups: [{ name, size, created }] }` | | `POST` | `/servers/{id}/backup` | `202` — snapshot now | | `POST` | `/servers/{id}/backups/{name}/restore` | `202` — world is snapshotted before the overwrite | | `DELETE` | `/servers/{id}/backups/{name}` | `{ ok: true }` | | `GET` | `/servers/{id}/files?path=` | `{ entries: [{ name, isDir, size, modified }] }` — `path` defaults to the instance root | | `GET` | `/servers/{id}/file?path=` | `{ content, mtime }` — mtime feeds the write conflict check | | `PUT` | `/servers/{id}/file` | body `{ path, content, expectedMtime? }` → `{ mtime }`; when `expectedMtime` doesn't match on-disk, returns an error starting with `conflict:` | | `POST` | `/servers/{id}/files` | body `{ op: "mkdir" \| "delete" \| "delete_recursive" \| "rename", path, to? }` | | `GET` | `/servers/{id}/search?q=&mode=contents\|filenames\|both&include=&exclude=` | `{ matches: [{ relPath, lineNumber?, linePreview? }] }` | | `GET` | `/servers/{id}/snapshots?path=` | `{ snapshots: [{ id, at, size }] }` — per-file editor history | | `GET` | `/servers/{id}/snapshot?path=&id=` | `{ content }` | | `POST` | `/servers/{id}/snapshots` | body `{ path }` → `{ id }` (`null` when nothing changed) | | `POST` | `/servers/{id}/snapshots/restore` | body `{ path, id }` | | `DELETE` | `/servers/{id}/snapshots` | body `{ path, id }` | | `PUT` | `/servers/{id}/tasks` | body `{ tasks: [...] }` — replaces the instance's schedule | | `GET` | `/servers/{id}/backup-schedule` | `{ intervalSecs, keep, onStop, lastBackupSecs }` | | `PUT` | `/servers/{id}/backup-schedule` | body = the same shape | | `GET` | `/servers/{id}/snippets` | `["say hi", ...]` | | `PUT` | `/servers/{id}/snippets` | body `{ snippets: [...] }` | | `GET` | `/servers/{id}/rcon` | `{ host, port, hasPassword }` | | `GET` | `/servers/{id}/players` | `{ players, raw }` — executes RCON `list` | | `GET` | `/servers/{id}/log/download` | raw `latest.log` with an attachment filename | | `POST` | `/plugins/upload-install?name=x.kern` | raw `.kern` body — validates + installs, returns the manifest summary | | `GET` | `/registry/plugins?q=&category=&sort=` | marketplace listing through the host's registry client | | `POST` | `/registry/install` | body `{ slug, version }` → `202 { jobId }` | | `GET` | `/jobs/{id}` | `{ id, kind, state: running\|done\|error, message, at }` | | `GET` | `/audit/download` | raw audit log with an attachment filename | | `GET` | `/inspect?path=` | import inspection: jars, start scripts, world/eula flags, suggested runtime/name | ### plugins | method | path | notes | |---|---|---| | `GET` | `/plugins` | installed manifests | | `POST` | `/plugins/install` | body `{ path, force? }` — path to a local `.kern` → `201` manifest | | `POST` | `/plugins/validate` | body `{ path }` → `{ valid, manifest \| error }` (never errors on a bad package) | | `DELETE` | `/plugins/{id}` | uninstall | ### streaming logs without re-reading `/servers/{id}/log` is offset-based. start with `offset=0` to get the tail, then keep the returned `nextOffset`: ```bash OFFSET=0 while true; do BODY=$(curl -s -H "Authorization: Bearer $TOKEN" \ "http://127.0.0.1:7442/servers/srv_123/log?lines=200&offset=$OFFSET") echo "$BODY" | jq -r '.lines[]' OFFSET=$(echo "$BODY" | jq -r .nextOffset) sleep 1 done ``` `reset: true` means the log rotated or shrank; the response contains a fresh tail — clear your buffer and continue from `nextOffset`. a trailing partial line is held back until it completes, so no output is ever split mid-line. ### long-poll events `/events` merges audit entries with the current status map. with `wait=30` it blocks until something new arrives or the wait elapses, which makes it a cheap push feed: ```bash SINCE=$(date +%s) while true; do BODY=$(curl -s -H "Authorization: Bearer $TOKEN" \ "http://127.0.0.1:7442/events?since=$SINCE&wait=30") echo "$BODY" | jq -r '.entries[] | "\(.at) \(.action) \(.detail)"' echo "$BODY" | jq -r '.statuses | to_entries[] | "\(.key): \(.value)"' SINCE=$(echo "$BODY" | jq -r .now) done ``` `entries` are oldest-first (`{ at, action, detail, serverId? }`). `statuses` is the full `id → status` map; diff consecutive responses to catch crash/restart transitions that don't produce an audit entry. `wait` is capped at 30 seconds; keep your http timeout above it. ### creating an instance end-to-end ```bash curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"name":"Prod API","serverType":"custom","path":"/srv/api","group":"prod","tags":["live"]}' \ http://127.0.0.1:7442/servers # → 201 { "id": "srv_a1b2c3", ... } curl -s -X POST -H "Authorization: Bearer $TOKEN" \ http://127.0.0.1:7442/servers/srv_a1b2c3/start ``` ## status codes | code | when | |---|---| | `200` | success | | `201` | created (server, plugin install) | | `202` | accepted — stop/restart/backup/restore run in the background | | `400` | invalid body, missing parameter, or a failed validation | | `401` | missing/incorrect bearer token | | `404` | unknown server/plugin/backup, or unknown route | | `413` / `431` | body / headers too large | | `500` | internal error | > **warn** the api cannot create or delete users (there are none) and never binds beyond `127.0.0.1`. for phone control over the lan see [web remote](/docs/web-remote) — a separate, token-paired https server. ## clients `kern-cli` wraps every endpoint with typed output. for anything it doesn't cover, `kern-cli api` is a raw passthrough: ```bash kern-cli api GET /servers kern-cli api PATCH /servers/srv_a1b2c3 --body '{"group":"staging"}' ``` # config.json kern's state lives in a single JSON document at `/config.json`: ```json { "version": "2.0.0", "settings": { "...": "..." }, "servers": { "srv_a1b2c3": { "id": "srv_a1b2c3", "...": "..." } } } ``` > **warn** the app owns this file. it rewrites it on every change, and manual edits made while kern is running are overwritten. prefer the ui or the [automation api](/docs/automation-api); edit the file only with kern closed. ## settings | key | type | default | meaning | |---|---|---|---| | `defaultSandboxPath` | string | `/servers` | where new instances go unless a custom path is chosen | | `launchOnLogin` | bool | `false` | register kern as an os-login item | | `closeToTray` | bool | `true` | close (×) hides to the tray instead of quitting | | `startHiddenInTray` | bool | `false` | stay hidden when launched by the os at login | | `trayRadar` | bool | `true` | animate the tray icon as a live radar | | `powerPricePerKwh` | number | `0` | local price; `0` disables the energy meter | | `machineWatts` | number | `120` | average draw for cost estimation | | `registryUrl` | string | `https://kern.aaenz.no` | plugin registry base url | | `webRemoteEnabled` | bool | `false` | serve the lan control panel | | `webRemoteBind` | string | `"0.0.0.0"` | interface the panel binds: `0.0.0.0` (all), `127.0.0.1` (localhost only — tunnel/reverse-proxy mode), or a specific ip | | `webRemotePort` | number | `7440` | https port for the web remote | | `nativeNotifications` | bool | `true` | mirror notifications to os toasts when unfocused | | `webhookUrl` | string | `""` | discord/slack/generic webhook | | `webhookEnabled` | bool | `false` | master switch for webhook delivery | | `logAlerts` | array | `[]` | `{ id, name, pattern, enabled }` regex rules over streamed logs | | `automationEnabled` | bool | `true` | serve the loopback [automation api](/docs/automation-api) | | `automationPort` | number | `7442` | loopback port | | `syncRepoUrl` | string | `""` | git remote for registry export/import | `webRemotePassphrase` is a legacy field kept so older files parse. ## instance fields each entry in `servers` is an instance. the fields you'll actually touch: | key | type | meaning | |---|---|---| | `id` | string | stable `srv_…` id | | `name` | string | display name | | `serverType` | string | plugin id (`custom`, `minecraft_java`, `discord_bot`, …) | | `path` | string | the instance folder | | `status` | string | `stopped` \| `running` \| `error` \| `stopped-forced` \| transitional states | | `isOrphaned` | bool | folder missing; set automatically | | `userOverrides` | object | config-form values, referenced as `{{userOverrides.*}}` | | `autoStart` | bool | launch with kern | | `stopCommand` | string? | stdin line for graceful stop; `""` skips stdin, absent uses the plugin default | | `stopTimeoutSecs` | number | graceful window before force-kill (default `30`) | | `group` | string? | sidebar group / fleet filter | | `tags` | string[] | lowercase labels; `--tag` filters | | `watchdog` | object | `{ enabled, maxAttempts }` crash auto-restart | | `tasks` | array | see [scheduled tasks](/docs/tasks) | | `backupSchedule` | object | `{ intervalSecs, keep, onStop, lastBackupSecs }` | | `alertRules` | object | `{ cpuThreshold, ramThreshold, sustainedSecs, crossedSinceSecs }` | | `rcon` | object | `{ host, port }` — password lives in the os keyring | | `commandHistory` / `commandSnippets` | string[] | terminal conveniences | | `lastPorts` | number[] | last observed listening ports (preflight) | host-managed fields (`status`, `pid`, `pidStarted`, `isOrphaned`, `lastBackupSecs`, `crossedSinceSecs`, `lastPorts`, task run stamps) are written by kern — `PATCH` and the ui preserve them even if your copy is stale. ## instance state after a crash if kern quits while servers are running, it **detaches** them rather than killing them. the next launch finds each persisted `pid` + `pidStarted`, verifies the process is still alive and identical (recycled pids fail the start-time check), and re-adopts it as a pid-only monitor. adopted instances show a distinct badge and support metrics and force-stop, but no stdin or log streaming — kern no longer owns their pipes. ## schema version `version` tracks the document schema. unknown fields are ignored, new fields default, so a config written by an older kern loads cleanly. downgrades aren't supported — back up `config.json` before rolling back. # faq **is kern free?** yes. no accounts, no tiers, no telemetry. **what can it actually run?** anything you can start from a folder with a command. officially-patterned plugins cover minecraft java (paper/purpur/fabric/forge/neoforge), discord bots in four runtimes, and generic node/rust/python services, but the engine is generic — a `custom` instance is just a command and a working directory. **does it need docker?** no. one process tree per instance, supervised directly. **where does my data live?** the app data dir (settings/plugins) and your instance folders (logs/backups/worlds). see [config.json](/docs/config-json). deleting kern never deletes your servers. **does quitting kern stop my servers?** no — they're deliberately detached and re-adopted on the next launch. use `kern-cli stop` / the stop button when you mean stop. **can i control it from my phone?** yes — the [web remote](/docs/web-remote): the full panel (console, files, backups, tasks, metrics) over https, paired with one-scan invites, or published anywhere through a cloudflare tunnel. **can i script it?** yes — [`kern-cli`](/docs/cli) for shells, and the [automation api](/docs/automation-api) for anything else, both on loopback with a bearer token. **is there a daemon / headless mode?** not yet. the app is the daemon; it runs in the tray. a headless build is a roadmap item. **how is this different from pterodactyl / amp?** those are web panels that manage servers on remote boxes with their own agents. kern is a native desktop app: your machine, your files, no docker, no browser, plus a plugin system for what "a server" means. **why is the installer unsigned?** code-signing certificates cost money and the project is free. the installer is unsigned, but **updates are minisign-signed and verified** — the risky part (silent self-update) is protected. **intel macs?** apple silicon only today. the dmg says so. **linux packages?** appimage for now; `.deb` is on the roadmap. **can two people share one kern?** it's a desktop app for one machine/user. multi-machine sync is export/import into a git repo, not real-time collaboration. **do health alerts and log alerts survive restarts?** the rules live in `config.json`, yes. the rolling metric history is in-memory and resets on exit — it's telemetry, not a database. **what happens if i edit config.json by hand?** with kern closed, it's honored on the next load; unknown fields are ignored and new fields default. with kern running, your edits race the app's writes — use the api or the ui. **can a plugin read my other servers or files outside its instance?** only if you granted `servers:read` / `files:read` and it uses them; file commands resolve paths under server directories. see [plugin security](/docs/plugin-security). **how do i uninstall cleanly?** uninstall the app, then delete the app data dir and any instance folders you created. nothing else is left behind. # glossary **instance** — a folder registered with kern, plus the settings that describe how to run it. one instance = one supervised process tree. **plugin** — a `.kern` package that teaches kern a server type: manifest metadata, a config form, lifecycle commands, optional ui. see `manifest-reference`. **`.kern` file** — the plugin archive (a zip). double-clicking one opens kern's install dialog via the `kern://` protocol. **lifecycle** — the named steps a plugin declares: `install`, `start`, `stop`, `restart`. resolved by rust at launch, with `{{userOverrides.*}}` templating. **userOverrides** — the values from the instance's config form (port, jar path, runtime…), referenced by lifecycle commands and scaffold files. **manifest** — `manifest.json` inside a plugin: id, version, permissions, config schema, lifecycle, scaffold. **host api** — the bridge a plugin ui uses to talk to the app (`invoke`, `listen`, tab/toolbar registrars), gated by manifest permissions. **orphaned** — the instance's folder is missing (moved, renamed, unplugged). kern keeps the record and flags it instead of deleting anything. **adopted** — a process that survived a kern restart with no pipes: it's monitored by pid (liveness, metrics, force-stop) but can't receive stdin or stream logs it didn't start. **graceful stop** — the stop pipeline: stdin/stop command → wait `stopTimeoutSecs` → force-kill the tree. the result of a forced path is `stopped-forced`. **stopped-forced** — the status when the graceful window expired and kern killed the tree. see `lifecycle`. **watchdog** — per-instance crash policy: auto-restart with exponential backoff up to `maxAttempts`, with notifications and a last-crash report. **preflight** — the read-only checks before a start: port conflicts (with the owning pid), pending minecraft eula, low disk. **snapshot / backup** — the `world/` directory zipped into `/backups/world-.zip`. a restore first snapshots the current world as `pre-restore-.zip`. **alert rules** — per-instance cpu/ram thresholds with a sustained window; firing raises a notification (and webhook). **notification center** — the in-app history of events (crashes, backups, alerts…), with optional native toasts and webhook mirroring. **audit log** — an append-only local record of lifecycle actions, config changes, plugin installs, backups, and task runs. **kern-cli** — the command line for a running kern. full-screen dashboard with no arguments. **automation api** — the loopback, bearer-token json api on `127.0.0.1` that `kern-cli` speaks. **web remote** — the optional https control panel on your lan, paired by qr. **reactor channel** — the cpu/ram telemetry bar in the instance header. amber above 90%, red on fault. **server type** — a plugin id (`custom`, `minecraft_java`, `discord_bot`). `custom` means "just run this command". # security model kern runs servers on your machine, so the trust boundaries worth knowing are concrete. ## the app itself - **no admin rights.** the windows installer is per-user (`%LOCALAPPDATA%`), macos and linux builds run unprivileged. kern never asks for elevation. - **no telemetry, no accounts.** nothing leaves the machine except the update check and the requests you configure (registry, webhooks). there is no kern cloud. - **files stay local.** config, logs, backups, and audit history live under the app data dir and your instance folders. deleting the app doesn't touch your server folders. ## the automation api - bound to **loopback only** (`127.0.0.1`) — nothing on the lan can reach it. - bearer-token auth; the token is a random 64-hex string written to `automation.json` in the app data dir and reused across restarts so scripts keep working. - plain http **on purpose**: loopback traffic never hits a network, so a self-signed https handshake would add trust prompts without adding a boundary. - **who can read the token:** anyone who can read your user's app data dir. that's the same trust level as your user account — files in `~` are already readable by processes running as you. ## the web remote see [web remote](/docs/web-remote). short version: https with a self-signed cert (or a real one via the tunnel), **per-device tokens** paired with single-use invites, roles (`viewer` / `operator` / `admin`) with optional per-server scoping, and no listener on the lan at all if you bind `127.0.0.1`. the optional **cloudflare tunnel** (quick or named) exposes it publicly — still device-token gated, but a public URL plus a valid token is full control, so enable it deliberately, revoke devices you don't trust, and put **cloudflare access** in front for anything long-lived. every remote mutation is attributed in the audit log. ## secrets - plugin secrets and stored credentials go to the **os credential vault** (windows credential manager / macos keychain / secret service), never to `config.json`. - the rcon password is stored in the vault too. - `config.json` holds no passwords. instance `.env` files are user content and are never uploaded or synced by kern. ## plugins plugins are a **capability boundary, not a sandbox** — plugin ui shares the host webview realm. the boundary works by allowlist: every host command maps to a permission the manifest must declare and you must consent to at install time. read [plugin security](/docs/plugin-security) before installing community packages. ## updates the in-app updater verifies a **minisign signature** against a public key embedded in the binary before installing anything. if verification fails, the update is refused. keys are never shipped to clients. ## backups archives are plain zips under `/backups/` — confidential if your world is. backups are not encrypted and not uploaded anywhere; treat them like the world folder itself. ## audit lifecycle actions, config changes, plugin installs, backups, and task runs are appended to `audit.log` in the app data dir (bounded and rotated). it's local-only, exportable from settings, and a good way to answer "who restarted this at 2am". ## reporting security issues: open a [github issue](https://github.com/aaen-studios/kern/issues) for non-sensitive reports; for anything exploitable, use github's private vulnerability reporting on the repo. # architecture kern is a tauri v2 desktop app: a rust backend (`src-tauri/`) and a react frontend (`src/`) in one window, plus out-of-process surfaces for scripts and phones. ``` ┌─ kern (tauri) ───────────────────────────────────────────────┐ │ react ui ──invoke/events── rust core │ │ ├─ registry (config.json) │ │ ├─ process supervisor │ │ ├─ scheduler (30s worker) │ │ ├─ plugin host (shadow dom) │ │ ├─ automation api (127.0.0.1) │ │ ├─ web remote (https, lan) │ │ └─ tray + updater │ └──────────────────────────────────────────────────────────────┘ │ spawn/stop ▲ plugins talk via HostAPI ▼ │ server processes (own trees) .kern plugin bundles ``` ## process model each instance is one supervised process **tree**: - **spawn.** the plugin's lifecycle command is resolved with `{{userOverrides.*}}` templating, wrapped in a hidden console (windows), and started in the instance folder. a generation counter stamps the launch so stale reader threads can't touch a restarted process. - **stream.** stdout and stderr are read on background threads, appended to `/latest.log`, and forwarded to the ui as stream events. a crash writes `crashes/.json` with the exit code and a bounded log tail. - **stop.** send the graceful command (stdin and/or the plugin's `stop` step) → wait `stopTimeoutSecs` → force-kill the whole tree. the tree is contained from birth: a **job object** on windows, a **process group** on unix, so a stop takes the children with it even if a plugin spawns helpers. - **detach.** quitting kern does not kill servers. their pipes are closed and the pid + start time persist in `config.json`; the next launch re-adopts verified survivors as pid-only monitors (metrics + force-stop, no stdin/logs). ## logs `latest.log` is an append-only plain-text mirror of the instance's output; the terminal pane renders it with ansi colors. readers (ui, cli, web remote) only ever read a bounded tail (2 MiB / 2000 lines) so a multi-gigabyte log never blocks anything. backups and crash reports take their own bounded slices. rotation is the server software's job — kern appends. ## plugins a `.kern` package is a zip: `manifest.json` + an optional esm ui bundle. the host mounts plugin ui in a **shadow root** (styles isolated), and every host call goes through `HostAPI.invoke`, an allowlist keyed by manifest permissions. see the plugin development section and [plugin security](/docs/plugin-security). ## background workers | worker | cadence | responsibilities | |---|---|---| | scheduler | 30s | scheduled tasks, health alerts, due backups + retention, metric history sampling | | watchdog | event-driven | crash restart with exponential backoff, give-up notifications | | logwatch | per line | user regex rules over streamed logs (throttled per rule) | | automation api | per request | loopback json api for `kern-cli` | | web remote | per request | https mobile page on the lan | | tray radar | 0.25s | animated tray icon from the metrics pipeline | all workers are best-effort: a failure is logged and never takes the app down. ## storage layout ``` / ├─ config.json registry + settings ├─ automation.json loopback port + bearer token ├─ audit.log bounded action history (+ .1 rotation) ├─ crashes/ per-instance last-crash reports ├─ plugins/ installed .kern contents ├─ web_remote/ self-signed cert material └─ crashes, ui state, sync scratch / ├─ latest.log appended live ├─ world/ game/server data └─ backups/ world-*.zip + pre-restore-*.zip ``` secrets never appear in either tree — rcon passwords and plugin secrets live in the os credential vault. ## frontend react 19 + vite + tailwind v4. state is deliberately plain: component state plus a small persistence layer, with the rust side as the source of truth for anything durable. ui state (window geometry, open tabs, filters) is persisted separately so a restart restores your place.