mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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>
64 lines
2.6 KiB
TypeScript
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));
|
|
}
|