Files
Codeman/src/config/multiuser.ts
T
Codeman maintainer f496e35d71 feat(multiuser): phase 1, user store, mode plumbing, CLI
Opt-in multi-user foundation (off by default; no behavior change without
CODEMAN_MULTIUSER/--multiuser):

- src/config/multiuser.ts: isMultiUserMode(), getUserSpacesDir()/userCasesDir(),
  maxUsers(), maxSessionsPerUser() (per-user fairness cap = global/2).
- src/types/user.ts: UserRecord/PasswordHash/AuthUser/PublicUser/UserRole.
- src/user-store.ts: ~/.codeman/users.json (atomic tmp+rename, mode 0600, short
  TTL cache). scrypt hashing with per-record params + timingSafeEqual verify plus
  rehash detection; createUser/setPassword/updateUser/deleteUser with last-admin
  invariants; guarded deleteUserSpace (symlink + realpath confinement, section 8);
  pure section-6.3 resolvers (resolveClaudeModeForUser downgrades bypass to auto
  for non-granted users; canRunPrivilegedCommands); bootstrapInitialAdmin.
- src/cli.ts: "codeman users add|passwd|list|rm" (hidden prompt or
  --password-stdin) operating directly on users.json; a --multiuser flag on the
  web command.

Tests: test/user-store.test.ts (29 tests: hashing/verify/rehash, username
validation, atomic 0600 write, last-admin invariants, 6.3 resolvers,
delete-space guards, bootstrap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 02:58:51 +02:00

64 lines
2.6 KiB
TypeScript

/**
* @fileoverview Multi-user mode gating + limits (opt-in, off by default).
*
* Multi-user mode is enabled by `codeman web --multiuser` (which sets
* `CODEMAN_MULTIUSER=1`) or the env var directly. When OFF, behavior is
* byte-identical to today: `users.json` is never read and all ownership scoping
* is bypassed. Everything here is per-instance like the rest of Codeman: a beta
* instance (`CODEMAN_INSTANCE=beta`) has its own `users.json` via `dataPath()`,
* and its user spaces live under the same shared `~/codeman-users` as prod (like
* `~/codeman-cases`), unless `CODEMAN_USER_SPACES_DIR` overrides it.
*
* See `docs/multi-user-plan.md` sections 3, 4.2, and 11.
*/
import { homedir } from 'node:os';
import { join } from 'node:path';
import { MAX_CONCURRENT_SESSIONS } from './map-limits.js';
/**
* Whether multi-user mode is active. Read from the environment each call so it is
* stable for the process lifetime (env does not change after boot) and trivially
* overridable in tests. Accepts `1` or `true`.
*/
export function isMultiUserMode(): boolean {
const v = process.env.CODEMAN_MULTIUSER;
return v === '1' || v === 'true';
}
/**
* Root of per-user spaces: `~/codeman-users` (sibling of `~/codeman-cases`).
* Overridable via `CODEMAN_USER_SPACES_DIR` (used by tests). Resolved lazily so a
* test can point it at a temp dir before the first call.
*/
export function getUserSpacesDir(): string {
return process.env.CODEMAN_USER_SPACES_DIR || join(homedir(), 'codeman-users');
}
/** Absolute path to a user's top-level space: `<USER_SPACES_DIR>/<username>[/segments]`. */
export function userSpacePath(username: string, ...segments: string[]): string {
return join(getUserSpacesDir(), username, ...segments);
}
/** Absolute path to a user's cases dir: `<USER_SPACES_DIR>/<username>/cases`. */
export function userCasesDir(username: string): string {
return join(getUserSpacesDir(), username, 'cases');
}
/** Maximum number of user accounts (default 25, env `CODEMAN_MAX_USERS`). */
export function maxUsers(): number {
const n = Number(process.env.CODEMAN_MAX_USERS);
return Number.isInteger(n) && n > 0 ? n : 25;
}
/**
* Per-user concurrent-session cap (the fairness lever). Defaults to half the
* global cap; overridable via `CODEMAN_MAX_SESSIONS_PER_USER`. The global cap
* (MAX_CONCURRENT_SESSIONS) still applies on top and is shared across users.
*/
export function maxSessionsPerUser(): number {
const n = Number(process.env.CODEMAN_MAX_SESSIONS_PER_USER);
if (Number.isInteger(n) && n > 0) return n;
return Math.max(1, Math.floor(MAX_CONCURRENT_SESSIONS / 2));
}