Skip to content

Multi-user configuration: eliminate single-user env var defaults #196

@dcellison

Description

@dcellison

Overview

Kai was built as a single-user system. One person, one machine, one .env file with every setting in it. That worked fine when there was one user and one admin and they were the same person.

Now Kai has multiple users. The project is public, external contributors have joined, and people are running their own instances. The single-user assumptions are baked into every layer of configuration, and they create real problems:

Users can't manage their own experience. Model preference, budget ceiling, timeout, workspace access, GitHub notification routing - all of these are per-user concerns, but they live in /etc/kai/env or root-owned YAML files. A user who wants to switch from Sonnet to Opus, add a workspace, or change where their GitHub notifications go has to ask the machine owner to edit a config file. That's not a minor inconvenience; it's a fundamental UX failure for a bot that's supposed to be self-service via Telegram.

The admin has no boundaries to set. In single-user, there's no distinction between "what the machine owner controls" and "what the user controls." In multi-user, the admin needs to set ceilings (max budget, allowed OS user, role) while letting users freely configure everything else. Today there's no mechanism for this - every setting is either fully locked in env or fully open.

GitHub integration doesn't know who cares about what. PR reviews, issue triage, and notifications all use global env vars. When a webhook event arrives, the system has no concept of which user is subscribed to which repo. All notifications go to a single hardcoded chat ID (GITHUB_NOTIFY_CHAT_ID). With multiple users working on different repos, this is fundamentally broken.

Workspace access is globally controlled. WORKSPACE_BASE and ALLOWED_WORKSPACES are single env vars shared by everyone. One user's scattered repos can't be allowed without also allowing them for all other users. And users can't add their own workspaces without admin intervention.

Configuration is scattered across three sources with no clear ownership. Settings live in .env, users.yaml, and workspaces.yaml with overlapping concerns and no consistent precedence model. Some settings exist in multiple places (e.g., CLAUDE_MODEL in env vs model in workspaces.yaml) with ad-hoc override behavior.

The goal

Every setting falls cleanly into one of three categories:

  1. Truly global (.env) - infrastructure, server capabilities, resource limits. Things that describe the machine itself: bot token, webhook port, whether whisper-cpp is installed, how long sessions can run. The machine owner sets these once. Users never see or think about them.

  2. Admin baseline (users.yaml, workspaces.yaml) - per-user and per-workspace defaults that the admin configures. Roles, OS user mapping, budget ceilings, repo associations, workspace pre-configuration. These are boundaries and starting points, not the final word.

  3. User preferences (database, set via Telegram) - everything a user should control about their own experience. Model, budget, timeout, workspace access, GitHub subscriptions, notification routing. Set via Telegram commands, persisted in the database, overriding the admin baseline within its boundaries.

The .env file shrinks dramatically. The YAML files become admin tools for onboarding and policy. Telegram becomes the primary interface for all user-facing configuration. No user ever needs to touch a file, ask for SSH access, or request an admin edit to customize their experience.

Current state

Setting Source Should be
ALLOWED_USER_IDS env users.yaml (admin) - #193
CLAUDE_MODEL env Per-user default in users.yaml, override via Telegram - #201
CLAUDE_MAX_BUDGET_USD env Per-user default in users.yaml (max_budget already exists), override via Telegram - #201
CLAUDE_TIMEOUT_SECONDS env Per-user default in users.yaml, override via Telegram - #201
CLAUDE_USER env users.yaml (os_user already exists) - admin only
ALLOWED_WORKSPACES env Per-user, user-managed via Telegram, stored in database - #202
WORKSPACE_BASE env Per-user in users.yaml (workspace_base) - #202
PR_REVIEW_ENABLED env Per-user in users.yaml (tied to GitHub identity) - #203
ISSUE_TRIAGE_ENABLED env Per-user in users.yaml (tied to GitHub identity) - #203
GITHUB_NOTIFY_CHAT_ID env Per-user in users.yaml (github_notify_chat_id), override via Telegram - #203
Workspace model/budget/timeout/env/prompt workspaces.yaml Telegram commands - #195

Design principles

  1. Telegram is the interface. Users manage their own settings via commands. No file editing required.
  2. users.yaml is the admin baseline. Admins set roles, limits, and defaults. Users can override within those boundaries.
  3. Database stores overrides. User preferences persist across restarts via the settings table.
  4. Env file shrinks to truly global settings. Bot token, webhook config, server capabilities, resource limits. Nothing per-user.
  5. YAML files stay as admin tools. users.yaml and workspaces.yaml remain for admins who want to pre-configure users and workspaces. They are baselines, not the primary interface. Example files (users.yaml.example, workspaces.example.yaml) are removed once the wizard and Telegram commands replace them.
  6. Backward compatibility. Env vars remain as global defaults. Per-user config takes precedence when set. Existing single-user installs work unchanged.

Env vars that stay in .env (truly global)

These are infrastructure, server capabilities, and resource limits with no per-user dimension:

Infrastructure/transport:

  • TELEGRAM_BOT_TOKEN, TELEGRAM_TRANSPORT, TELEGRAM_WEBHOOK_URL, TELEGRAM_WEBHOOK_SECRET
  • WEBHOOK_PORT, WEBHOOK_SECRET
  • KAI_INSTALL_DIR, KAI_DATA_DIR
  • TOTP_* settings

Server capabilities (depends on what is installed on the machine, not user preference):

  • VOICE_ENABLED, TTS_ENABLED

Resource limits (machine owner decides, never user-visible):

  • CLAUDE_MAX_SESSION_HOURS
  • CLAUDE_IDLE_TIMEOUT
  • PR_REVIEW_COOLDOWN (rate limit on the review agent, not a user preference)
  • FILE_RETENTION_DAYS

User-to-repo mapping

GitHub-related settings (PR_REVIEW_ENABLED, ISSUE_TRIAGE_ENABLED) are tied to a user's GitHub identity. Currently the webhook processes events with no awareness of which user cares about which repo.

For true multi-user support, users.yaml needs a way to associate users with GitHub repos and route notifications to their chosen destination:

users:
  - telegram_id: 2114582497
    name: Daniel
    role: admin
    github: dcellison
    os_user: kai
    home_workspace: /Users/kai/Projects/kai
    workspace_base: /Users/kai/Projects
    max_budget: 10
    model: sonnet
    timeout: 300
    github_repos:
      - dcellison/kai
      - dcellison/anvil
    github_notify_chat_id: -100123456789
    pr_review: true
    issue_triage: true

When a GitHub webhook event arrives for dcellison/kai, the system looks up which user(s) have that repo in their github_repos list, then routes notifications to their github_notify_chat_id (or falls back to their telegram_id if unset).

github_notify_chat_id preserves the separation between GitHub notifications and the user's main Kai chat (PR #182). Each user controls where their notifications go, either via users.yaml or /github notify <id>.

This also opens the door for users to manage their own repo subscriptions via Telegram (e.g., /github add dcellison/kai, /github remove dcellison/anvil).

Webhook ingestion

The routing above only works if events actually arrive. GitHub repos must have a webhook configured pointing to Kai's endpoint (https://api.syrinx.net/webhook/github) with the correct WEBHOOK_SECRET. Today only dcellison/kai has this.

For multi-user, /github add <owner/repo> handles both sides: subscribe the user to notifications AND register the webhook on the repo via the GitHub API. This requires a per-user GitHub token with admin:repo_hook scope, stored via /github token <ghp_...> (one-time setup, persisted in database). If no token is available, the command still subscribes the user but falls back to manual setup instructions. All repos share the same WEBHOOK_SECRET for signature validation. See #203 for the full registration flow, token storage, manual fallback, and deregistration logic.

Allowed workspaces

Users manage their own allowed workspace list via Telegram commands. These are paths outside workspace_base that they want access to (repos in scattered locations, etc.).

  • /workspace allow /path/to/repo - adds a path (must exist and be readable by the user's os_user)
  • /workspace deny /path/to/repo - removes it
  • Stored in the database per-user
  • The filesystem (os_user permissions) is the real security boundary

Precedence model (highest to lowest)

  1. Database (user-set via Telegram)
  2. YAML files (admin-set baselines: users.yaml for user config, workspaces.yaml for workspace config)
  3. Env file (global defaults)
  4. Hardcoded defaults (in config.py)

New Telegram commands summary

Command Purpose Issue
/settings View/set per-user defaults (model, budget, timeout) #201
/settings model <name> Set default model #201
/settings budget <n> Set budget ceiling #201
/settings timeout <n> Set timeout #201
/settings reset [key] Clear overrides #201
/workspace config View/set per-workspace settings #195
/workspace config model/budget/timeout/env/prompt Set workspace-specific overrides #195
/workspace allow <path> Add allowed workspace #202
/workspace deny <path> Remove allowed workspace #202
/workspace allowed List allowed workspaces #202
/github View repo subscriptions and GitHub settings #203
/github token <ghp_...> Store GitHub PAT for webhook management #203
/github add <owner/repo> Subscribe + register webhook on repo #203
/github remove <owner/repo> Unsubscribe + deregister webhook if last subscriber #203
/github notify <chat_id> Set notification destination #203
/github reviews on/off Toggle PR review notifications #203
/github triage on/off Toggle issue triage notifications #203

Sub-issues

The work breaks into ordered phases:

  1. Config wizard generates users.yaml - Replace ALLOWED_USER_IDS with users.yaml in config wizard #193. Minimal bootstrap: telegram_id, name, role, os_user, home_workspace, workspace_base. Remove users.yaml.example.
  2. Workspace config via Telegram - Manage per-workspace config via Telegram commands #195. /workspace config commands for model, budget, timeout, env, prompt. Remove workspaces.example.yaml. workspaces.yaml stays as admin baseline.
  3. Per-user settings via Telegram - Per-user settings via /settings command #201. /settings command for model, budget, timeout. Unify with existing /model command.
  4. Per-user allowed workspaces - Per-user allowed workspaces via Telegram commands #202. /workspace allow/deny/allowed commands. Move WORKSPACE_BASE to per-user workspace_base in users.yaml. Deprecate ALLOWED_WORKSPACES env var.
  5. User-to-repo mapping and GitHub notification routing - User-to-repo mapping and GitHub notification routing #203. /github command for repo subscriptions and notification routing. Route webhook events by repo ownership.
  6. Deprecate single-user env vars - update docs, deprecation warnings, eventual removal
  7. Final cleanup - remove any remaining backward-compatibility shims once migration period ends

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions