kenny — Setup & Operations¶
How to host kenny-server, configure it, build and distribute kenny-agent, and cut releases.
For day-to-day operator use, see user-guide.md.
Topology¶
flowchart TB
subgraph Cloud["Small cloud host (e.g. OCI Free Tier)"]
Proxy["TLS proxy (Caddy)<br/>:443 https/wss"]
subgraph Compose["docker compose"]
Srv["kenny-server :8000"]
Vol[("volume: /data<br/>kenny.sqlite")]
end
Proxy --> Srv
Srv --- Vol
end
Op(("Operator<br/>browser / Claude")) -->|https| Proxy
PC1["kenny-agent<br/>(Windows PC)"] -->|wss, dials out| Proxy
PC2["kenny-agent<br/>(Windows PC)"] -->|wss, dials out| Proxy
LX["kenny-agent<br/>(Linux host)"] -->|wss, dials out| Proxy
Anthropic["Anthropic API"] -. chat .- Srv
Prerequisites¶
- A host with Docker + Docker Compose (server), reachable by the agents over TLS.
- A DNS name + TLS for production (the bundled Caddy profile can obtain certs automatically).
- For the AI features (Ask kenny, recommendations, forecast prose, event classification, the ticket assistant, triage): an Anthropic API key — in the environment as
ANTHROPIC_API_KEY, or set later in Admin → AI — or an AI gateway that holds one. - To build the agent / cut releases: a GitHub repo with Actions (the workflow targets
windows-latest).
Quick start (Docker Compose)¶
# from the repo root
KENNY_OPERATOR_TOKEN="$(openssl rand -hex 24)" \
KENNY_AGENT_TOKENS="example-pc=$(openssl rand -hex 16),example-laptop=$(openssl rand -hex 16)" \
ANTHROPIC_API_KEY="sk-ant-..." \
docker compose up --build -d
The server is now on http://localhost:8000 (data persists on the kenny-data volume). Open /
and complete first-run setup: the first account you create becomes the superuser
(ADR-0033). From there a superuser manages accounts under Admin → Users
(roles superuser / operator / user, per-user host scope, and personal access tokens).
Claude Desktop connects to /mcp through kenny's built-in OAuth 2.1 flow
(ADR-0037): add a custom connector with the
https://<server>/mcp URL, sign in with your kenny account, and approve once — no token to paste.
Scripts and other MCP clients can still send a per-user access token (Authorization: Bearer <pat>)
instead. The KENNY_OPERATOR_TOKEN(S) below stay accepted as a back-compat superuser so existing
installs upgrade with no manual steps. For TLS in front (port 443, wss), enable the Caddy profile:
Behind a reverse proxy, set KENNY_FORWARDED_ALLOW_IPS to the proxy's address so the
login rate-limiter throttles by the real client IP rather than the proxy's (otherwise all
clients share one bucket). The bundled TLS profile sets this for you.
Environment variables¶
Some settings are also editable in the dashboard's Admin page; the
tables below name the Admin section for each one. A value saved in Admin wins over the
environment until it is reset there. Every other variable is environment-only: it never
appears in the dashboard, and a stored dashboard value for it is dropped with a warning at
startup. A background loop whose interval is environment-only (0 disables it) reads that
interval at startup.
| Variable | Used by | Default | Purpose |
|---|---|---|---|
KENNY_OPERATOR_TOKEN |
server | insecure dev fallback | Legacy shared bearer token; still accepted as a back-compat superuser for MCP + /api after the upgrade to accounts (ADR-0033). Deprecated in favour of per-user access tokens. |
KENNY_OPERATOR_TOKENS |
server | — | Optional comma-separated set of additional accepted shared tokens (each a back-compat superuser). |
KENNY_SESSION_TTL_SECS |
server | 604800 |
Browser login session lifetime in seconds (default 7 days). |
KENNY_OAUTH_ACCESS_TTL_SECS |
server | 3600 |
Lifetime of an OAuth access token issued to a connected MCP client (default 1 hour). |
KENNY_OAUTH_REFRESH_TTL_SECS |
server | 2592000 |
Lifetime of a rotating OAuth refresh token (default 30 days); reuse of a rotated token revokes the whole grant. |
KENNY_AGENT_TOKENS |
server | dev map | id=token,id2=token2 — per-agent tokens (the token store is seeded from this). |
ANTHROPIC_API_KEY |
server | — | Enables the AI features. Also settable in Admin → AI, where a saved key wins over this one; a key saved there is not included in backups. The master switch there (KENNY_AI_ENABLED, default 1) turns every AI feature off at once. |
KENNY_CHAT_MODEL |
server | claude-sonnet-4-6 |
Model for Ask kenny, the ticket assistant and triage. Also editable in Admin → AI. |
KENNY_FAST_MODEL |
server | claude-haiku-5-5 |
Model for recommendations, forecast prose and reliability-event classification. Also editable in Admin → AI. |
ANTHROPIC_BASE_URL |
server | — | Base URL of an AI gateway; empty talks to the Anthropic API directly. Also editable in Admin → AI. |
ANTHROPIC_CUSTOM_HEADERS |
server | — | Headers sent with every model call, for the AI gateway: Name: Value entries separated by ; or newlines. Also settable in Admin → AI; a value saved there is never shown back and not included in backups. |
KENNY_TLS |
server | unset | Set 1 behind TLS so the login cookie gets the Secure flag. |
KENNY_FORWARDED_ALLOW_IPS |
server | 127.0.0.1 |
Upstream proxy address(es) allowed to set X-Forwarded-For, so the login rate-limiter sees the real client IP behind a reverse proxy (not the proxy's). Set to your proxy's address when fronting kenny with the Caddy TLS profile. |
KENNY_PUBLIC_URL |
server | http://localhost:<port> |
External base URL; used to build installer/update links, the agent --server wss://…/agent/ws, and the OAuth issuer / discovery-metadata / resource URLs. Set it to your public https://… origin so Claude Desktop's OAuth flow advertises reachable endpoints. |
KENNY_AGENT_BINARY |
server | — | Path to the prebuilt kenny-agent.exe the server serves for Windows installer download + self-update. Overrides the GitHub auto-fetch when set. |
KENNY_AGENT_BINARY_LINUX |
server | — | Path to the prebuilt Linux x86_64 agent binary (static musl) the server serves for the Linux install script + self-update. Overrides the GitHub auto-fetch when set. |
KENNY_AGENT_BINARY_LINUX_AARCH64 |
server | — | As above for Linux aarch64 (Raspberry Pi / ARM NAS). |
KENNY_GITHUB_TOKEN |
server | — | Token for polling a private kenny-server package on GHCR (ADR-0040). The agent binary and the changelog are read from GitHub anonymously (ADR-0057) and ignore it. |
KENNY_GITHUB_REPO |
server | nullthrone/kenny |
Repo to fetch the agent binary release from. |
KENNY_AGENT_BINARY_CACHE |
server | <dir of KENNY_DB_PATH>/kenny-agent.exe |
Where the auto-fetched binary is cached (the /data volume in the container). |
KENNY_AGENT_VERSION |
server | 0.2.0 |
Fallback version label only — the GitHub release tag of the fetched binary leads (ADR-0015). Used when no tag is known (e.g. a manually-placed binary without a .version sidecar). |
KENNY_HOST / KENNY_PORT |
server | 127.0.0.1 / 8000 |
Bind address (container sets 0.0.0.0). |
KENNY_DB_PATH |
server | kenny.sqlite |
SQLite store (snapshots, events, tokens, keys, chat, web filter — one file). Container: /data/kenny.sqlite. |
KENNY_TELEMETRY_RETENTION_DAYS |
server | 30 |
How long raw telemetry snapshots are kept (ADR-0051). Also editable in Admin → System. Snapshots dominate this database's size (~90 KB/row); lowering this is the main lever on disk usage. A decrease prunes on the next alert cycle (~60s); DELETE frees space for reuse but does not shrink the file — restore from a backup or VACUUM offline to reclaim disk. |
KENNY_HW_HISTORY_RETENTION_DAYS |
server | 730 |
How long the compact per-device daily hardware metrics are kept (ADR-0070): disk wear and error counters, GPU link width, fan speed per duty band and hardware-error counts, rolled up from the snapshots once a day. The wear-out, counter and fan-drift forecasts are fitted on them, so a short window hides slow trends. Minimum 30; a few kilobytes per host per month. Also editable in Admin → System; a decrease prunes on the next alert cycle. |
KENNY_SQLITE_BUSY_TIMEOUT_MS |
server | 20000 |
How long a write waits for a contended SQLite lock before raising "database is locked" (ADR-0051). Read once at process start; not editable at runtime. |
KENNY_TELEMETRY_INTERVAL_SECS |
agent / server | 900 |
Agent push interval; also pre-filled into generated installers. |
KENNY_COEXIST_ENABLED |
agent | 1 |
Anti-cheat coexistence (ADR-0035): while a protected game runs, the agent suspends screen_capture (returns paused) and relaxes process/port telemetry. Set 0 to disable. |
KENNY_COEXIST_PROCESSES |
agent | anti-cheat set | Comma-separated extra process names to treat as "a protected game is running", extending the built-in anti-cheat list (EasyAntiCheat.exe, BEService*.exe, …). Add game exes here, e.g. ARC-Raiders.exe. Matched case- and .exe-insensitively. |
KENNY_COEXIST_POLL_SECS |
agent | 5 |
How often the agent checks whether a watched process is running. |
KENNY_COEXIST_TELEMETRY_INTERVAL_SECS |
agent | 3600 |
Telemetry push interval while a protected game is running (never shorter than KENNY_TELEMETRY_INTERVAL_SECS). |
KENNY_SERVER_VERSION |
server | 0.0.0-dev |
Version string shown on the sidebar's fleet line, in the About kenny dialog, and in /api/about. |
KENNY_LOG_LEVEL |
server | INFO |
Root log level. Server logs are also persisted to the event store (ADR-0017). Also editable in Admin → System, where a change applies immediately. |
Agent authentication & identity (ADR-0022 mutual Ed25519 auth, token rotation):
| Variable | Default | Purpose |
|---|---|---|
KENNY_SERVER_PRIVATE_KEY |
generated + logged | Base64 32-byte Ed25519 seed — the server identity agents pin. If unset the server generates one at startup and logs the public key to pin in installers; set it (or KENNY_SERVER_PRIVATE_KEY_FILE) to keep identity stable across restarts. |
KENNY_TOKEN_GRACE_SECS |
604800 |
Grace window (7 d) during which a rotated agent token still works; 0 = instant invalidation. |
KENNY_KEY_GRACE_SECS |
604800 |
Grace window for a rotated agent public key. |
KENNY_ALLOW_TOKEN_AUTH |
1 |
Accept the legacy bearer-token agent handshake during migration (disable once all agents are enrolled). |
KENNY_MAX_FRAME_BYTES |
8388608 |
Absolute inbound frame ceiling (8 MiB). |
KENNY_MAX_TELEMETRY_BYTES |
262144 |
Per-push byte cap (256 KiB). |
KENNY_MAX_TELEMETRY_SECTIONS |
128 |
Max sections per snapshot. |
Alerting, digest & notifications (see Alerting & digests). Every
key here except KENNY_ALERT_INTERVAL_SECS is also editable in Admin → Alerts &
notifications:
| Variable | Default | Purpose |
|---|---|---|
KENNY_ALERT_INTERVAL_SECS |
60 |
Alert-evaluation loop interval; 0 disables alerting. Environment-only, read at startup. |
KENNY_ALERT_COOLDOWN_SECS |
3600 |
Per-scope flap-suppression cooldown for warn transitions. |
KENNY_ALERT_OFFLINE_AFTER_SECS |
2700 |
Mark an agent offline after this long without a push (≈ three missed 15-min pushes). Offline pauses health evaluation and notifies no one. |
KENNY_ALERT_MISSING_AFTER_DAYS |
7 |
Report a host with no telemetry for this many days as missing (daily summary + ticket). |
KENNY_ALERT_DAILY_HOUR |
8 |
Hour (UTC) of the daily summary of warnings, missing hosts and disk forecasts. |
KENNY_ALERT_CHANGE_PUSH |
local_accounts,autostart:added,scheduled_tasks:added,browser_extensions:added |
Inventory changes (section or section:kind) that push; the rest are only recorded. |
KENNY_DIGEST_ENABLED |
1 |
Weekly digest on/off. |
KENNY_DIGEST_DAY / KENNY_DIGEST_HOUR |
mon / 8 |
When to send the weekly digest. |
KENNY_NTFY_URL / KENNY_NTFY_TOKEN |
— | ntfy topic URL (+ optional bearer) for push alerts. |
KENNY_WEBHOOK_URL |
— | Generic JSON webhook for alerts. |
Database backups (see the Backup section, ADR-0039):
| Variable | Default | Purpose |
|---|---|---|
KENNY_BACKUP_INTERVAL_SECS |
21600 (6 h) |
Scheduled-backup loop interval; 0 pauses automatic backups. Also editable in Admin → Backup; re-read live, so a change there retimes, pauses or resumes the running loop. |
KENNY_BACKUP_INITIAL_DELAY |
30 |
Delay before the first scheduled backup after startup. Environment-only. |
KENNY_BACKUP_RETENTION |
7 |
Snapshots kept per target before older ones are pruned. Also editable in Admin → Backup. |
KENNY_BACKUP_DIR |
<dir of KENNY_DB_PATH>/backups |
Where local snapshots are written — the directory to point an external sync tool at. Environment-only. |
Parental controls / web filter (see Parental controls):
| Variable | Default | Purpose |
|---|---|---|
KENNY_WEBFILTER_REFRESH_SECS |
86400 |
External list refresh interval; 0 disables. Environment-only, read at startup. |
KENNY_WEBFILTER_ADULT_URL |
StevenBlack list | Source URL for the porn-only hosts list. Also editable in Admin → Web filter. |
KENNY_WEBFILTER_BYPASS_URL |
hagezi list | Source URL for the DoH/VPN/proxy bypass list. Also editable in Admin → Web filter. |
KENNY_WEBFILTER_GAMBLING_URL |
hagezi list | Source URL for the gambling list. Also editable in Admin → Web filter. |
KENNY_WEBFILTER_PIRACY_URL |
blocklistproject list | Source URL for the piracy/torrent list. Also editable in Admin → Web filter. |
KENNY_WEBFILTER_MAX_BLOCK_DOMAINS |
5000 |
Cap on external domains of the capped categories pushed to an agent (hard cap 10 000). Also editable in Admin → Web filter. |
KENNY_WEBFILTER_DEFAULT_HISTORY |
violations |
History a PC starts with when first heard from: violations keeps only matches against its list, full keeps every observed domain. Changing it affects new PCs only. Also editable in Admin → Web filter. |
A changed source URL applies on the next list refresh.
Discord bot & tickets (see Tickets & the Discord bot for the full
operator setup walkthrough — creating the Discord application, the two privileged intents,
and both enrollment paths). Tickets themselves need none of this: the ticket store,
lifecycle service and sweeper always run, and the dashboard's Inbox and /api/tickets
work with nothing configured here. These keys only decide whether a Discord bot is also
connected, install the optional dependency first: pip install -e ".[discord]".
| Variable | Default | Purpose |
|---|---|---|
KENNY_DISCORD_BOT_TOKEN |
— | Bot token of the Discord application. Unset keeps the whole Discord surface off. |
KENNY_DISCORD_ENABLED |
0 |
Connect the bot at startup. Needs the token above and at least one allowed guild. |
KENNY_DISCORD_GUILD_IDS |
— | Comma-separated guild (server) snowflakes kenny reacts in. Empty means deny everywhere — there is no allow-all mode. |
KENNY_DISCORD_SUPPORT_CHANNEL_ID |
— | Channel a mention has to be in to open a ticket; empty accepts a mention in any channel of an allowed guild. |
KENNY_DISCORD_OPERATOR_CHANNEL_ID |
— | Where operator approval cards are posted; empty posts them into the ticket's own thread. |
KENNY_DISCORD_PRIVATE_THREADS |
1 |
Open each ticket in a private thread with only the requester invited. |
KENNY_DISCORD_MODEL |
— | Anthropic model id for the Discord surface; empty falls back to KENNY_CHAT_MODEL. |
KENNY_DISCORD_MAX_TURNS_PER_TICKET |
40 |
Autonomous turn cap per ticket before it is handed to an operator. Ticket-wide (any turn on the ticket, from Discord or the dashboard's ticket chat), except an operator+-driven turn from either surface never counts against it. |
KENNY_DISCORD_RATE_LIMIT_PER_USER_HOUR |
20 |
Per-account throttle on opening/driving tickets, ticket-wide across both surfaces; 0 = unlimited. An operator+-driven turn, from either surface, is exempt. |
KENNY_DISCORD_WEBHOOK_URL |
— | Discord incoming-webhook URL for the alert push channel — independent of the bot. Also editable in Admin → Alerts & notifications; see Alerting & digests. |
KENNY_TICKET_APPROVAL_TTL_SECS |
86400 |
How long a held approval/consent waits for a decision before the sweeper expires it (an expiry counts as a denial); 0 never expires. |
KENNY_TICKET_AUTOCLOSE_SECS |
172800 |
Reopen window: a resolved ticket untouched this long is auto-closed; 0 disables. |
KENNY_TICKET_STALL_NUDGE_SECS |
172800 |
A ticket blocked on a reply (from the requester or an operator) this long gets one reminder; 0 disables reminders. |
KENNY_TICKET_STALL_GIVEUP_SECS |
604800 |
A ticket still waiting on the requester after this long is re-blocked on an operator instead; 0 disables escalation. |
KENNY_TICKET_ABANDON_SECS |
1209600 |
A new/in_progress ticket neither an operator nor its requester has touched this long is cancelled, recorded as dropped rather than withdrawn. Machine activity does not count; a ticket on an approval gate is never dropped this way. 0 disables. See Tickets → the lifecycle. |
KENNY_TICKET_SWEEP_INTERVAL_SECS |
300 |
Ticket housekeeping loop interval (expires gates, nudges and escalates stalls, auto-closes, drops untouched tickets); 0 disables. Environment-only, read at startup. |
KENNY_TICKET_SWEEP_INITIAL_DELAY |
30 |
Delay before the first sweep after startup. Environment-only. |
KENNY_TICKET_RETENTION_DAYS |
30 |
How long a closed ticket keeps its raw working transcript. The ticket, its summary and its audit trail are never pruned. |
KENNY_TRIAGE_ENABLED |
1 |
On a new ticket, run one read-only investigation on its PC and write the finding into the ticket before anyone is asked to look. Needs an Anthropic API key or an AI gateway (Admin → AI, or ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL); without either it stays off whatever this says. See Tickets → kenny looks first. |
KENNY_TRIAGE_RESOLVE |
0 |
Let an investigation set a ticket to resolved itself — only for an alert-opened ticket, only on a closing verdict, and only when a read-only check actually ran and succeeded. Off means every verdict is a recommendation. |
KENNY_TRIAGE_MAX_ITERATIONS |
8 |
Model round-trips one investigation may take. Spending them all produces no verdict: the ticket stays open with what was found. |
Every key in this table except the bot token and the two KENNY_TICKET_SWEEP_* keys
(environment-only) is also editable in the dashboard's Admin page:
- Admin → Discord —
KENNY_DISCORD_ENABLED,KENNY_DISCORD_GUILD_IDS, the support and operator channel IDs,KENNY_DISCORD_PRIVATE_THREADS, andKENNY_DISCORD_RATE_LIMIT_PER_USER_HOUR. - Admin → AI —
KENNY_DISCORD_MODEL,KENNY_DISCORD_MAX_TURNS_PER_TICKET(shown as Assistant turns per ticket), and the threeKENNY_TRIAGE_*keys. - Admin → Tickets — the approval TTL, auto-close, stall, and abandon lifetimes, and
KENNY_TICKET_RETENTION_DAYS. - Admin → Alerts & notifications —
KENNY_DISCORD_WEBHOOK_URL.
A change applies immediately, except KENNY_DISCORD_ENABLED, which applies on the next
restart; the dashboard marks a stored change that waits for one as RESTART PENDING.
Security: if
KENNY_OPERATOR_TOKENis unset the server uses a loud, insecure dev token. Always set real tokens and serve overwss/httpsfor anything non-local. Seeadr/0008-operator-authentication.mdandadr/0014-auth-hardening.md.
AI gateway¶
kenny can send every model call through an AI gateway you run or subscribe to, instead of straight to the Anthropic API — for central quotas, inline inspection of prompts and answers, and an audit trail outside kenny (ADR-0068). Everything is set in Admin → AI (or the environment) and applies to the next call, without a restart.
What the gateway must do. Serve the Anthropic Messages API at <base URL>/v1/messages
and pass it through intact: streaming (server-sent events), tool use, extended thinking,
output_config (the effort setting) and cache_control. A gateway that rejects the effort
setting still works: kenny drops it after the first refusal and the fast model thinks at its
own default. A gateway that only speaks another API format does not work with kenny.
Configure it to answer an upstream error — including overload during a stream — with the
error status, not with 200: kenny retries and reports on the status.
What kenny needs from you.
| Setting | What to put there |
|---|---|
AI gateway URL (ANTHROPIC_BASE_URL) |
The gateway's base URL, without /v1/messages, e.g. https://gateway.example or https://gateway.example/tenant/kenny. https is required, except to a host on the same machine or container network (http://localhost:8080, http://gateway:8080). |
AI gateway headers (ANTHROPIC_CUSTOM_HEADERS) |
The headers the gateway authenticates and routes by, e.g. X-Gateway-Key: …; X-Gateway-Route: kenny. Never shown back and never in a backup — set them again after a restore. |
| Anthropic API key | Empty if the gateway holds the provider key (kenny then sends a placeholder); otherwise the key, which kenny sends to the gateway. |
| Chat model / Fast model | The model ids the gateway expects, if it names models its own way. |
Headers the HTTP client owns (Host, Content-Length, Connection, …), X-Api-Key and
Anthropic-Version cannot be set this way.
Check it. Test connection in Admin → AI sends one single-token message on the fast model through the gateway — the same path, headers, model id and request shape every feature uses — and shows the gateway's answer verbatim if it refuses. The status line above it names the gateway's host.
What the gateway sees. Everything the model sees: telemetry facts, event-log samples, web-activity host names, ticket text and tool output. Choose a gateway you would trust with the model provider's role. kenny's approval gates do not depend on the model: a state-changing tool call still waits for an operator, whatever the gateway returns.
Running from source (development)¶
# server
cd kenny-server && pip install -e ".[dev]"
KENNY_OPERATOR_TOKEN=dev KENNY_HOST=127.0.0.1 KENNY_PORT=8000 kenny-server
# agent (foreground; Linux builds via cfg fallbacks)
cd kenny-agent && cargo run -- --server ws://127.0.0.1:8000/agent/ws \
--agent-id dev --token dev-token --telemetry-interval-secs 30
/e2e runs a real agent↔server smoke test; /contract-check verifies both sides match the wire
contract and golden fixtures.
Enabling agent downloads from the GUI¶
The server serves a prebuilt binary; it does not compile per download (see
adr/0012-agent-distribution-prebuilt-binary.md). Point it at a binary and set the
public URL:
# compose.yaml (server service) — excerpt
environment:
KENNY_PUBLIC_URL: https://kenny.example.com
KENNY_AGENT_BINARY: /data/kenny-agent.exe # mount/copy the release artifact here
KENNY_AGENT_VERSION: "0.2.0"
Then the dashboard's download installer / share link / update agent buttons work. The
installer bundles setup.bat + a kenny-agent.setup.json sidecar carrying the per-agent
--server, --agent-id, a minted one-time --enroll-token, and the pinned --server-pubkey. The
relative double-clicks setup.bat; the agent self-elevates and installs itself into
%ProgramFiles%\kenny (see
adr/0030-agent-self-elevating-bootstrap-installer.md).
The Add a PC wizard's second step asks for the target OS. For a Linux target, point
the server at a Linux binary as well and the wizard's hand-over step produces a one-line
install command instead of a ZIP (see
Installing the agent on Linux and
adr/0034-linux-agent-distribution-convenience-script.md):
environment:
KENNY_AGENT_BINARY_LINUX: /data/kenny-agent-linux-x86_64 # static musl x86_64
KENNY_AGENT_BINARY_LINUX_AARCH64: /data/kenny-agent-linux-aarch64 # optional, Raspberry Pi / ARM NAS
Auto-fetch from GitHub (no manual binary placement)¶
To avoid the first-agent chicken-and-egg (hand-placing the .exe into the volume before any
installer can be downloaded), the server can fetch the binary itself when a GitHub token is
configured (ADR-0015). No credential is involved — releases are read anonymously (ADR-0057):
On startup (and via the dashboard's retry GitHub fetch button) the server downloads the latest
release's agent binaries — kenny-agent-<tag>-x86_64-pc-windows-msvc.exe and the Linux
…-<arch>-unknown-linux-musl variants — verifies each against its published .sha256, and caches
them on the /data volume. The fetch is best-effort and per-asset — if the repo is unreachable
the dashboard shows a banner with manual instructions instead. Operator-placed
KENNY_AGENT_BINARY / KENNY_AGENT_BINARY_LINUX always win over the fetched cache. The dashboard's
Add a PC wizard lets you onboard the very first machine without a pre-existing agent.
Best-effort does not mean quiet. Every attempt is recorded, including the one branch that decides not to fetch at all (an operator-placed binary taking precedence), and the outcome survives a restart. About kenny shows it on the staged agent version row and Fleet's banner repeats it, so a stale staged version always comes with the reason it stopped moving.
Two failures are worth recognising by name. A 403 is either GitHub's rate limiter or a refusal;
the dashboard says which, and for the limiter it names the reset time. Because reads are anonymous
the limit is 60 requests per hour per IP, shared with anything else behind the same address —
kenny's own draw is a fraction of that. A 404 means the repo is not public: auto-fetch cannot
reach a private release repo at all, and that deployment needs KENNY_AGENT_BINARY placed by hand.
Installing the agent on Windows¶
The normal path is the dashboard bundle: double-click setup.bat and approve the Windows
security prompt. setup reads kenny-agent.setup.json, elevates via UAC, copies the binary into
%ProgramFiles%\kenny, and registers the auto-start service — no unzip-and-right-click ritual (see
adr/0030-agent-self-elevating-bootstrap-installer.md
and adr/0013-agent-windows-service-and-self-update.md).
The single binary manages its own service. For manual/debugging use:
kenny-agent.exe setup # self-elevating install (config from kenny-agent.setup.json,
# or pass the flags below explicitly)
# equivalent explicit install (run as Administrator):
kenny-agent.exe install --server wss://kenny.example.com/agent/ws `
--agent-id example-pc --server-pubkey <base64> --enroll-token <token> `
[--telemetry-interval-secs 900] [--service-name kenny-agent]
kenny-agent.exe uninstall # remove the service (and the %ProgramFiles%\kenny install dir)
kenny-agent.exe run # foreground (default when no subcommand) — for debugging
--server-pubkey pins the server identity and --enroll-token is the one-time enrollment secret
(ADR-0022); a bare legacy --token is only accepted during the migration window. install writes
kenny-agent.config.json next to the exe and registers an auto-start service with
restart-on-failure recovery. Updates are pushed from the server (no manual reinstall).
Installing the agent on Linux¶
kenny-agent runs on Linux hosts too — headless servers, a NAS, a Raspberry Pi, or a Linux desktop — reporting into the same fleet through the same server (ADR-0031). The agent is a static musl binary with no runtime dependencies, installed as a systemd service. Distribution follows the Docker/K3s convenience-script model (ADR-0034).
The normal path is the dashboard's one-line install command. In the Add a PC wizard, name the machine, pick Linux, and copy the command its hand-over step produces — then run it on the target host as root:
The nonce-gated, single-use script carries the per-agent --server, --agent-id, a minted
one-time --enroll-token, and the pinned --server-pubkey. It detects the CPU architecture
(x86_64 / aarch64), downloads the matching binary from the server, and runs kenny-agent
setup, which copies the binary into /opt/kenny, writes its config to /etc/kenny, and enables
an auto-start systemd unit. On first run the agent generates its Ed25519 keypair and enrolls its
public key. Verify with:
systemctl status kenny-agent # unit active, ExecStart=/opt/kenny/kenny-agent run-service
journalctl -u kenny-agent -f # follow the agent log
The single binary manages its own service. For a manual / air-gapped install, download
kenny-agent-<tag>-<arch>-unknown-linux-musl from the
latest release, then (as root):
chmod +x kenny-agent-*-unknown-linux-musl
sudo ./kenny-agent-*-unknown-linux-musl setup \
--server wss://kenny.example.com/agent/ws --agent-id study-pi \
--server-pubkey <base64> --enroll-token <token> [--telemetry-interval-secs 900]
sudo kenny-agent uninstall # disable + remove the systemd unit
Paths on Linux: binary in /opt/kenny, config in /etc/kenny, state/key in /var/lib/kenny,
logs in /var/log/kenny. There is no tray kill-switch or session-0/desktop launch on Linux (those
are Windows-only, ADR-0031); a headless server needs neither.
Upgrades are server-triggered, exactly like Windows — click update on the agent in the
dashboard (or POST /api/agents/{id}/update). The agent downloads the new binary, verifies its
SHA-256, atomically swaps /opt/kenny/kenny-agent, and restarts its systemd unit; no manual step
on the host (ADR-0034).
Releases (GHCR image + agent binaries)¶
Tag a version to publish (.github/workflows/release.yml):
flowchart LR
Tag["git tag v*"] --> RW["release.yml"]
RW --> Img["server image →<br/>ghcr.io/<owner>/kenny-server:<tag>"]
RW --> Exe["kenny-agent.exe<br/>(windows-latest)"]
RW --> Lnx["kenny-agent musl<br/>(x86_64 + aarch64)"]
Exe --> Sha["+ SHA256"]
Exe --> Sign["+ Authenticode<br/>(if cert secret set)"]
Lnx --> LSha["+ SHA256"]
Sha & Sign & LSha --> Rel["GitHub Release asset"]
- The server image lands in GHCR (semver +
latest, with provenance). - The Windows agent binary is built on
windows-latest, hashed, optionally code-signed whenWINDOWS_CERT_BASE64/WINDOWS_CERT_PASSWORDrepo secrets are set, and attached to the Release. - The Linux agent binaries are cross-built as static musl artifacts (
cross),kenny-agent-<tag>-x86_64-unknown-linux-musland…-aarch64-unknown-linux-musl, each hashed and attached to the Release. The x86_64 build is e2e-gated before publish. - Pull the release binaries to the host and point
KENNY_AGENT_BINARY(Windows) andKENNY_AGENT_BINARY_LINUX/KENNY_AGENT_BINARY_LINUX_AARCH64(Linux) at them to enable GUI downloads/updates against that version — only needed for a private repo, since the server auto-fetches all of them from a public one on its own.
Dev channel (ADR-0048)¶
Every push to main additionally publishes a prerelease build (.github/workflows/release-dev.yml,
tag v<next-patch>-dev.<run_number>) via the same shared job body
(.github/workflows/_release-artifacts.yml) and the same publish gate (image smoke test, e2e
against the exact release binary) as a stable tag — nothing about how an artifact is verified
differs by channel. Because GitHub Releases marks it prerelease: true, the stable resolution
path (GET /repos/{repo}/releases/latest) never sees it, so the dev channel cannot affect stable
agents or the stable server image. The reverse does not hold: the dev channel serves the
highest release by semver across both kinds, so a stable release cut after the last main
push (say v2.5.0 after v2.4.1-dev.88) is what a dev-channel PC gets until the next dev build
outranks it. A dev-channel PC is never held behind the stable fleet, and it may run a
stable-built binary, which then reports channel: stable while its desired channel stays dev.
The server image also gets a floating :edge GHCR tag as a convenience docker pull alias;
nothing server-side resolves or pins against it — detection always uses the exact versioned
tag + digest.
To run one PC on dev while the rest of the fleet stays stable: set that agent's desired channel
to dev in the dashboard (or POST /api/agents/{id}), then approve a dev-channel update campaign
the same way as a stable one. Only agents whose desired channel matches the campaign's channel are
eligible — a dev campaign never touches an agent an operator hasn't opted in. A locally built agent
can also self-identify as dev from the start via KENNY_AGENT_CHANNEL=dev cargo build --release
(stamped into register.meta.channel and the os_support telemetry section).
Code signing (Authenticode)¶
An unsigned agent binary is more likely to be flagged by AV and game anti-cheat heuristics, and carries no verifiable publisher identity. The Windows build already carries PE identity metadata (CompanyName/ProductName/version + icon, ADR-0035); signing it is the complementary step and is wired but off by default:
- Set the
WINDOWS_CERT_BASE64(base64 of the signing cert) andWINDOWS_CERT_PASSWORDrepo secrets;release.ymlthen Authenticode-signskenny-agent.exewith a timestamp. The server ships the exe unmodified (ADR-0030/0012), so the signature survives distribution and self-update. - Use a real OV or EV code-signing certificate whose subject matches the VERSIONINFO
CompanyName. Since the 2023 CA/Browser-Forum change, code-signing keys must live on
FIPS-140-2 hardware, so a plain PFX-in-secret may need swapping for a cloud-signing service
(e.g. Azure Trusted Signing, DigiCert KeyLocker, SSL.com eSigner) invoked via
signtool. - A formal anti-cheat allowlist is generally not available to a self-hosted family tool; signing + the coexistence back-off (ADR-0035) are the practical levers. Never attempt to evade an anti-cheat — that risks banning the player's game account.
Persistence, backups, upgrades¶
- Data: the SQLite telemetry store lives on the
kenny-datavolume (/data). Telemetry snapshots auto-prune after ~30 days. - Backups: kenny has a built-in backup manager (ADR-0039,
dashboard Admin → Backup section, superuser only) — do not point an external
sync/backup tool at
kenny.sqlitedirectly; syncing the live WAL file causes lock contention. Instead, on a schedule (KENNY_BACKUP_INTERVAL_SECS, default 6 h) or on demand, it writes a consistentVACUUM INTOsnapshot to<KENNY_DB_PATH dir>/backups/— this directory holds only finished, static files and is what you should point Syncthing/rsync/whatever at. Optionally fan snapshots out to a remote HTTP/SCP-SFTP/FTP(S) target too, configured from the same section. Restore stages a chosen backup and restarts the server to apply it (see the Backup section reference). - Server upgrade:
docker compose pull && docker compose up -d(or bump the image tag). The Admin → Updates section (below) tells you when a newer tag exists and gives you the exact, digest-pinned command — it never pulls or restarts the container for you. - Agent upgrade: click update on one agent in the dashboard (server-triggered self-update, unchanged) — or approve a fleet-wide update campaign from the Admin → Updates section to roll a pinned version out to every agent, on both Windows and Linux (ADR-0034, ADR-0040).
Scheduled updates (ADR-0040)¶
kenny checks for newer agent releases (GitHub Releases) and a newer server image (GHCR tags, read-only) on a schedule, and surfaces both from the dashboard's Admin → Updates section (operator role or higher). Detection never applies anything by itself:
- Server: GHCR is polled for a newer semver tag than the one running; the page shows it with
the exact, digest-pinned
docker pull …@sha256:… && docker compose up -dfor you to run. A container cannot replace its own running image, so this stays a shown command rather than an automated pull in this iteration (a docker-socket-holding auto-apply sidecar is a deferred, additive follow-up — see the ADR). - Agents: approving a rollout pins one exact version — the operator's approval names an artifact, not a subscription, so a release found by a later check never ships under an already-approved campaign. From the pinned campaign you can push to every currently-online agent with apply now, and/or turn on auto-apply on connect so agents get the pinned version as they reconnect while the campaign is active. A campaign auto-expires once every known agent is updated (or after a configurable max age) and can be revoked at any time — revoking stops future pushes only, an update already in flight to an agent cannot be recalled. An agent that keeps refusing (e.g. its local remote-control kill switch is off, ADR-0011) is marked held after a few attempts instead of being retried forever.
| Variable | Default | Purpose |
|---|---|---|
KENNY_UPDATE_CHECK_INTERVAL_SECS |
86400 (24 h) |
Scheduled update-check loop interval; 0 disables. Environment-only, read at startup. |
KENNY_UPDATE_CHECK_INITIAL_DELAY |
30 |
Delay before the first check after startup. Environment-only. |
KENNY_SERVER_IMAGE_REF |
ghcr.io/nullthrone/kenny-server |
GHCR image polled for a newer server tag. Environment-only. |
KENNY_AGENT_ROLLOUT_ON_CONNECT |
0 |
Auto-apply an active, approved campaign to agents as they connect. Off by default — a campaign must still be approved first either way. Also switched in Admin → Updates (auto-apply on connect in the agent rollout card). |
KENNY_UPDATE_CAMPAIGN_MAX_AGE_SECS |
1209600 (14 d) |
A campaign auto-expires after this long even if not every agent was reached. Also editable in Admin → Updates. |
Dependencies & security automation¶
- Dependabot (
.github/dependabot.yml) opens weekly update PRs for pip, cargo, GitHub Actions, and the Docker base image. - Run
/security-reviewto audit kenny's weak points and file deduplicated GitHub issues.
CI¶
.github/workflows/ci.yml runs the server tests + lint, the agent fmt/clippy/test/build, a
Windows job for the #[cfg(windows)] code, and a real agent↔server e2e job on every PR.