Summary
The per-user configuration system has accreted into three orthogonal axes over several PRs, each individually reasonable. In aggregate, the surface is harder to reason about than it should be, even for contributors who wrote it. This issue captures the current state, explains how it got here, and sketches a consolidation path. No urgency.
Current state
Three independent tiers of config precedence:
- Environment variables (
/etc/kai/env in a protected install).
users.yaml (per-user admin-set).
settings table in the SQLite DB (per-user user-writable via /settings).
Which tier owns a given setting varies per-setting. Some examples:
There is no uniform rule; each setting must be looked up individually.
Two parallel workspace-access mechanisms:
workspace_base: one directory under which /workspace <name> does name resolution.
allowed_workspaces: a per-user list of explicit pinned paths reachable by short name.
- Tiebreaker: base wins on name collision.
These solve adjacent problems but are not a single feature.
Roughly fifteen per-user fields:
telegram_id, name, role, github, os_user, home_workspace, max_budget, model, timeout, context_window, workspace_base, agent_backend, llm_provider, github_repos, github_notify_chat_id, pr_review, issue_triage.
Several names do not telegraph their behavior. home_workspace sounds like a container; it is actually a single starting path. workspace_base sounds like a single path; it is actually a container root for name resolution. A contributor reading users.yaml for the first time has to go source-diving to disambiguate.
How we got here
Each step was locally sensible:
The cumulative surface is the sum of decisions that individually made sense.
Symptom
- The project author has difficulty reasoning about which tier owns what.
- The Multi-User Setup wiki page plus the macOS onboarding guide together document a roughly fifteen-by-three matrix of fields and tiers.
- Adding a new user on macOS currently requires edits across multiple files in multiple locations with several verification steps.
When documentation complexity is emergent rather than designed, docs are downstream of the real problem.
Potential fix (sketch)
Three consolidation moves, orthogonal and shippable independently:
-
Make users.yaml the single per-user source of truth. Env vars stay as global-default-for-single-user only. /settings writes into users.yaml (or a user-owned overlay file), not a separate DB table. Collapses three tiers to two.
-
Merge workspace_base and allowed_workspaces into one workspaces: list. First entry is the landing workspace; the rest are pins. Name resolution walks the list. Eliminates the collision-tiebreaker rule and the two-mechanism mental model.
-
Rename fields whose names mislead. Candidates: home_workspace to something that telegraphs "starting directory"; workspace_base to something that telegraphs "root for name resolution." Renames are cheap to automate with a migration pass over existing users.yaml files.
Non-goals
- Not urgent. No deadline attached.
- Not one PR. Each collapse can ship independently.
- Existing multi-user setups should migrate automatically; no user-visible behavior change by default.
Related
Summary
The per-user configuration system has accreted into three orthogonal axes over several PRs, each individually reasonable. In aggregate, the surface is harder to reason about than it should be, even for contributors who wrote it. This issue captures the current state, explains how it got here, and sketches a consolidation path. No urgency.
Current state
Three independent tiers of config precedence:
/etc/kai/envin a protected install).users.yaml(per-user admin-set).settingstable in the SQLite DB (per-user user-writable via/settings).Which tier owns a given setting varies per-setting. Some examples:
max_budget: users.yaml only.model: all three tiers.workspace_base: env or users.yaml.budget ceiling: env only (intentional, post-Rename CLAUDE_MAX_BUDGET_USD to BUDGET_CEILING, separate ceiling from default #305).There is no uniform rule; each setting must be looked up individually.
Two parallel workspace-access mechanisms:
workspace_base: one directory under which/workspace <name>does name resolution.allowed_workspaces: a per-user list of explicit pinned paths reachable by short name.These solve adjacent problems but are not a single feature.
Roughly fifteen per-user fields:
telegram_id,name,role,github,os_user,home_workspace,max_budget,model,timeout,context_window,workspace_base,agent_backend,llm_provider,github_repos,github_notify_chat_id,pr_review,issue_triage.Several names do not telegraph their behavior.
home_workspacesounds like a container; it is actually a single starting path.workspace_basesounds like a single path; it is actually a container root for name resolution. A contributor readingusers.yamlfor the first time has to go source-diving to disambiguate.How we got here
Each step was locally sensible:
users.yamlas the per-user config source.users.yamlfor per-user settings./settingsadded a user-writable DB tier so users could adjust their own defaults at runtime.workspace_baseandallowed_workspaceswere added at different times to solve different onboarding pain points.The cumulative surface is the sum of decisions that individually made sense.
Symptom
When documentation complexity is emergent rather than designed, docs are downstream of the real problem.
Potential fix (sketch)
Three consolidation moves, orthogonal and shippable independently:
Make
users.yamlthe single per-user source of truth. Env vars stay as global-default-for-single-user only./settingswrites intousers.yaml(or a user-owned overlay file), not a separate DB table. Collapses three tiers to two.Merge
workspace_baseandallowed_workspacesinto oneworkspaces:list. First entry is the landing workspace; the rest are pins. Name resolution walks the list. Eliminates the collision-tiebreaker rule and the two-mechanism mental model.Rename fields whose names mislead. Candidates:
home_workspaceto something that telegraphs "starting directory";workspace_baseto something that telegraphs "root for name resolution." Renames are cheap to automate with a migration pass over existingusers.yamlfiles.Non-goals
Related