Skip to content

Protected Installation

Daniel Ellison edited this page Sep 3, 2026 · 1 revision

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.

Why use a protected installation?

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 - .env equivalent lives at /etc/kai/env with 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).

Prerequisites

  • 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 two-step workflow

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.

Step 1: Configure

Run from the project directory as your normal user:

python -m kai install config

This 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 whole users.yaml before writing install.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

Step 2: Apply

Run as root:

sudo python -m kai install apply

This reads install.conf and performs the installation in order:

  1. 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"
  2. Stops the service if it's already running
  3. Creates directories with correct ownership and permissions (see Directory layout), including per-principal home workspaces under the data directory, seeded with AGENTS.md and owned by each person's os_user
  4. Copies source to the install location (clean copy, excludes __pycache__, .pyc, .git, .venv, .env)
  5. Creates or updates the venv with Python 3.13+; on update, only rebuilds if pyproject.toml changed (checksum comparison)
  6. Copies model files (whisper, Piper) if present in the source directory
  7. 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
  8. Configures sudoers at /etc/sudoers.d/kai (validated with visudo -cf before writing)
  9. Migrates runtime data from dev layout if this is the first protected install (database and logs; never overwrites existing files)
  10. Generates and installs the service definition (launchd plist or systemd unit)
  11. Starts the service

Dry run

Preview everything without making changes:

sudo python -m kai install apply --dry-run

Every action is printed with a [DRY RUN] prefix. Always use this on your first attempt.

Step 3: Verify

Run as your normal user:

python -m kai install status

This 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).

Directory layout

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.

User separation (os_user)

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.

What it does

  • 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/kill rule 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

What Claude can and cannot access

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)

Setup

  1. Create the OS user(s) before running apply (e.g., per-user accounts like alice, bob); the preflight requires them to exist
  2. Set os_user on each user entry in users.yaml
  3. Run sudo make install - it validates isolation, then generates the sudoers rules automatically
  4. 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.

Runtime access grants

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.

Client surfaces

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.

Updating an existing installation

Re-run the same two steps:

  1. python -m kai install config - only if config values changed (existing values are shown as defaults)
  2. sudo python -m kai install apply - handles updates automatically:
    • Source is re-copied (clean copy every time)
    • Venv is only rebuilt if pyproject.toml changed
    • Secrets are re-written
    • Service is stopped before changes and started after
    • Database is never wiped on update

macOS vs Linux

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.

Troubleshooting

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.

Clone this wiki locally