-
Notifications
You must be signed in to change notification settings - Fork 8
Description
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:
-
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. -
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. -
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
- Telegram is the interface. Users manage their own settings via commands. No file editing required.
- users.yaml is the admin baseline. Admins set roles, limits, and defaults. Users can override within those boundaries.
- Database stores overrides. User preferences persist across restarts via the settings table.
- Env file shrinks to truly global settings. Bot token, webhook config, server capabilities, resource limits. Nothing per-user.
- YAML files stay as admin tools.
users.yamlandworkspaces.yamlremain 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. - 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_SECRETWEBHOOK_PORT,WEBHOOK_SECRETKAI_INSTALL_DIR,KAI_DATA_DIRTOTP_*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_HOURSCLAUDE_IDLE_TIMEOUTPR_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: trueWhen 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'sos_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)
- Database (user-set via Telegram)
- YAML files (admin-set baselines: users.yaml for user config, workspaces.yaml for workspace config)
- Env file (global defaults)
- 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:
- 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. - Workspace config via Telegram - Manage per-workspace config via Telegram commands #195.
/workspace configcommands for model, budget, timeout, env, prompt. Removeworkspaces.example.yaml.workspaces.yamlstays as admin baseline. - Per-user settings via Telegram - Per-user settings via /settings command #201.
/settingscommand for model, budget, timeout. Unify with existing/modelcommand. - Per-user allowed workspaces - Per-user allowed workspaces via Telegram commands #202.
/workspace allow/deny/allowedcommands. MoveWORKSPACE_BASEto per-userworkspace_basein users.yaml. DeprecateALLOWED_WORKSPACESenv var. - User-to-repo mapping and GitHub notification routing - User-to-repo mapping and GitHub notification routing #203.
/githubcommand for repo subscriptions and notification routing. Route webhook events by repo ownership. - Deprecate single-user env vars - update docs, deprecation warnings, eventual removal
- Final cleanup - remove any remaining backward-compatibility shims once migration period ends