-
Notifications
You must be signed in to change notification settings - Fork 19
Protected Installation
A protected installation deploys Kai with hardened directory permissions, separating read-only source code, writable runtime data, and root-owned secrets across three locations. This is the recommended setup for always-on deployments where the bot runs as a system service.
In development mode (make run), everything lives under the project directory and runs as your user. That's fine for hacking, but for a persistent service you want stronger isolation:
- Source code is read-only - owned by root, the service user can't modify it
-
Secrets are root-owned -
.envequivalent lives at/etc/kai/envwith mode 0600; the service user reads it via targeted sudoers rules - Runtime data is separated - database, logs, and file exchange live in their own directory
-
The agent process can't access secrets - it runs as the service user (or optionally a separate user) with no access to
/etc/kai/
The installer handles both macOS (launchd) and Linux (systemd).
- Python 3.13+ on the host
- At least one agent backend binary installed where discovery finds it (any of the five; see Installing Agent Binaries)
- A Telegram bot token and your user ID, when the Telegram adapter is enabled; a workshop-only install needs neither (see client modes)
-
A dedicated OS user for the service (e.g.,
kai) - A distinct OS account per interactive user - protected mode requires one for every person; the apply step validates this before touching anything
- macOS or Linux - the installer detects and handles both
The installer uses a two-step process that separates privilege levels:
python -m kai install config # no sudo - interactive Q&A, writes install.conf
sudo python -m kai install apply # root - reads install.conf, creates the layout
python -m kai install status # no sudo - reports current state
install.conf bridges the two steps: config writes it, apply reads it. It's a JSON file stored in the project root with restricted permissions (mode 0600) since it contains secrets.
Run from the project directory as your normal user:
python -m kai install configThis walks through an interactive Q&A. If install.conf already exists, its values are used as defaults so re-running only asks about changes.
The wizard's sections, in order: deployment mode, client mode (hybrid / workshop-only / telegram-only), installation paths, Telegram (when enabled), user setup or the initial Workshop admin block, transport, default backend with its follow-ups, agent tunables, the webhook server (port, optional Workshop LAN address, split webhook secrets), workspaces, model catalogue, PR review limits, optional features, semantic memory, and external services. Configuration Wizard documents every prompt; two protected-mode specifics matter here:
-
Every interactive user needs a distinct
os_user. The wizard loops until each person has an OS account name that differs from the service account, and validates the wholeusers.yamlbefore writinginstall.conf. -
First-time files are staged, not written live: the wizard stages
users.yaml(and other protected YAML) under~/.cache/kai-install/so the root-level apply step can copy them into/etc/kai/.
After the prompts, the config is written to install.conf:
Configuration written to /path/to/kai/install.conf
Review the file, then run: sudo python -m kai install apply
Run as root:
sudo python -m kai install applyThis reads install.conf and performs the installation in order:
-
Validates user isolation first: every interactive user must have a distinct, existing
os_user. On failure the apply aborts with "no installation changes were made" - Stops the service if it's already running
-
Creates directories with correct ownership and permissions (see Directory layout), including per-principal home workspaces under the data directory, seeded with
AGENTS.mdand owned by each person'sos_user -
Copies source to the install location (clean copy, excludes
__pycache__,.pyc,.git,.venv,.env) -
Creates or updates the venv with Python 3.13+; on update, only rebuilds if
pyproject.tomlchanged (checksum comparison) - Copies model files (whisper, Piper) if present in the source directory
-
Writes secrets to
/etc/kai/env(mode 0600), copies staged YAML from~/.cache/kai-install/into/etc/kai/, discovers backend binaries and writes/etc/kai/backends.yaml, and validates or migrates/etc/kai/runtime-profiles.yaml -
Configures sudoers at
/etc/sudoers.d/kai(validated withvisudo -cfbefore writing) - Migrates runtime data from dev layout if this is the first protected install (database and logs; never overwrites existing files)
- Generates and installs the service definition (launchd plist or systemd unit)
- Starts the service
Preview everything without making changes:
sudo python -m kai install apply --dry-runEvery action is printed with a [DRY RUN] prefix. Always use this on your first attempt.
Run as your normal user:
python -m kai install statusThis reports the current installation state without requiring sudo. Expect a long report: after the paths, ownership, sudoers, service, and version lines, it prints the Workshop authority checks: runtime profiles, client adapters, webhook secret migration, model catalogue, runtime storage, and a status line per canonical authority (bootstrap, agent authority, handles, notifications, channel policy, unread, delivery, runtime sessions and cutovers, execution and operational state, preferences, memory authority, message integrity, transcript authority, legacy JSONL archive, channel lifecycle, DM archive, GitHub automation, integration routes, internal API authority, post-run effects, and the core schedule). Each line is content-free (counts and states, never message text), so the output is safe to share when asking for help.
It also checks workspace path traversal - if the service user can't traverse any parent directory of a configured workspace, you'll see a warning with the specific fix (e.g., chmod o+x /Users).
A protected installation creates this structure:
/opt/kai/ root:root 755 Read-only install tree
src/ root:root 755 Python source
venv/ root:root 755 Virtual environment
pyproject.toml root:root 644 Package metadata
run.sh root:root 755 Launcher script (macOS only)
models/ root:root 755 Whisper/Piper models (if present)
.pyproject.sha256 root:root 644 Venv update detection checksum
/var/lib/kai/ kai:kai 755 Runtime data
kai.db kai:kai 644 SQLite database (canonical event store,
projections, runs, delivery, sessions)
logs/ kai:kai 755 Log files (daily rotation)
home/ Per-person home workspaces
<principal_id>/ per os_user Seeded with AGENTS.md; owned by that person
files/ kai:kai 755 File exchange directory
<principal_id>/ Per-person upload subdirectories
history/ kai:kai 755 Conversation history archive
<channel_id>/ Per-channel JSONL (non-canonical archive)
memory/ kai:kai 755 Persistent memory
<principal_id>/ per os_user Per-person MEMORY.md
preferences/ Per-person PREFERENCES.md with revisions
<principal_id>/ per os_user
/etc/kai/ root:root 755 Secrets and admin-owned config
env root:root 600 Environment variables
backends.yaml root:root 644 Backend command registry (discovery-written)
runtime-profiles.yaml root:root 600 Protected runtime policy
memory-projects.yaml root:root 600 Memory project registry (if present)
services.yaml root:root 600 External service API keys
users.yaml root:root 600 Per-user config (Telegram transport)
workspaces.yaml root:root 600 Per-workspace config (if present)
totp.secret root:root 600 TOTP secret (if configured)
totp.attempts root:root 600 TOTP attempt tracking
The install tree carries no home workspace; apply removes a leftover <install>/home/ tree when upgrading an older install, and per-person home workspaces live under the data directory, each owned by its person's os_user. The canonical identity file is AGENTS.md (Claude reads it through a one-line import adapter).
The key principle: the service user (kai) can write its own runtime data but cannot read /etc/kai/env directly (it reads secrets via the NOPASSWD sudo cat rules the installer configured), and per-person files are owned by each person's account, with root-owned helper scripts granting the service user narrow read access where the daemon needs it (memory for backup, preferences for injection).
Runtime data lives entirely in /var/lib/kai/, never inside the install tree, so install apply can cleanly re-copy source without destroying conversation history or memory.
In a protected installation, process isolation is required: every interactive user must name an os_user, a distinct existing OS account their agent subprocess runs as, different from the service account and unique per person. The wizard enforces this at config time and the apply step re-validates it before making any change. The reason is structural: the service account can sudo cat exact protected Kai files, so an agent running as that identity would inherit those capabilities.
(The former CLAUDE_USER global env var is retired; os_user in users.yaml is the only lever. Single-user development mode keeps the historic optional same-user shape.)
See Multi-User Setup for the full multi-user configuration.
- The bot starts the agent with
sudo -H -u <os_user> ...instead of spawning it directly - The agent runs in a new process session (
start_new_session=True) so signals from the bot don't leak to it - The sudoers rules grant the service user each of the five agent binaries (claude, codex, opencode, goose, pi) as each configured OS user, in the form
kai ALL=(alice) CWD=* SETENV: NOPASSWD: <path>, plus a/bin/killrule for cross-user process cleanup - Root-read rules cover the protected config files the daemon must read (
env,backends.yaml,runtime-profiles.yaml,memory-projects.yaml), and two root-owned helper scripts give the service user narrow access to per-person memory and preference files when foreign target users exist
| Can access | Cannot access |
|---|---|
| Its own home directory |
/etc/kai/ (secrets) |
| The workspace directory | Kai's database (/var/lib/kai/kai.db) |
| Tools on PATH | Kai's source code (root-owned) |
- Create the OS user(s) before running apply (e.g., per-user accounts like
alice,bob); the preflight requires them to exist - Set
os_useron each user entry inusers.yaml - Run
sudo make install- it validates isolation, then generates the sudoers rules automatically - Make sure each backend binary is reachable by each OS user (the registry pins the paths; see Installing Agent Binaries)
The payoff is defense in depth: even a compromised agent can't read bot secrets or modify the database, and no person's subprocess can access another person's files.
make runtime-access (the third privileged command alongside apply and status) opens an interactive editor over /etc/kai/runtime-profiles.yaml for granting or revoking each person's authorized backend and provider options; people then switch among their granted options at runtime without any config edit. Never hand-edit the profiles file.
Which surfaces the installed service serves comes from the client mode chosen in the wizard (KAI_ENABLED_ADAPTERS). The HTTP server always starts and serves the Workshop browser client at http://127.0.0.1:8080/workshop/ when Workshop is enabled; WORKSHOP_LAN_HOST optionally adds a LAN listener carrying only the client routes. A workshop-only protected install is a fully supported shape: no bot token, no users.yaml, with the first admin provisioned by the wizard's bootstrap block and further people added via the workshop CLI.
Re-run the same two steps:
-
python -m kai install config- only if config values changed (existing values are shown as defaults) -
sudo python -m kai install apply- handles updates automatically:- Source is re-copied (clean copy every time)
- Venv is only rebuilt if
pyproject.tomlchanged - Secrets are re-written
- Service is stopped before changes and started after
- Database is never wiped on update
| Aspect | macOS | Linux |
|---|---|---|
| Service type | LaunchDaemon plist | systemd unit |
| Service location | /Library/LaunchDaemons/com.syrinx.kai.plist |
/etc/systemd/system/kai.service |
| Launcher script | Yes (run.sh) - needed because Homebrew Python re-execs through Python.app, changing the PID |
No - systemd tracks the process directly |
| Start/stop | launchctl bootstrap/bootout system/com.syrinx.kai |
systemctl start/stop kai |
| Auto-restart |
KeepAlive in plist |
Restart=always in unit |
| Boot start |
RunAtLoad in plist |
WantedBy=multi-user.target |
| Directory layout | Same | Same |
| Sudoers rules | Same (binary paths auto-detected) | Same |
The launcher script on macOS deserves a note: Homebrew Python's framework binary fork-execs through Python.app, creating a grandchild process with a new PID. Launchd loses track of this. The run.sh wrapper stays as the parent process launchd tracks and forwards SIGTERM to the real Python process for graceful shutdown.
Service won't start
Check kai.log in the data directory (/var/lib/kai/logs/). Common causes: missing claude binary on PATH, Python version mismatch, or permission errors on the workspace directory.
"Permission denied" reading /etc/kai/env
The sudoers rules may be missing or incorrect. Verify with sudo visudo -cf /etc/sudoers.d/kai. Re-run sudo python -m kai install apply to regenerate them.
Backend binary not found
The runtime resolves every backend command through /etc/kai/backends.yaml, and the sudoers rules pin the same paths. If you installed, moved, or reinstalled any agent binary, re-run sudo make install so discovery regenerates both. See Installing Agent Binaries for discovery locations.
"Python >= 3.13 required" during apply
The installer requires Python 3.13+. Install it and ensure it's the default python3 on your PATH, or install as python3.13 (the installer checks both).
Database not found after migration
The installer copies kai.db from the project root to /var/lib/kai/ on first apply. Existing files are never overwritten. If you see database errors, check that KAI_DATA_DIR is set correctly in the service environment.
Workspace traversal warnings
The installer and status command check that the service user can traverse every parent directory of configured workspace paths. If you see a warning like WARNING: /Users lacks execute permission for kai, follow the suggested fix (e.g., chmod o+x /Users).
Always dry-run first
Use sudo python -m kai install apply --dry-run to preview all changes before applying.