import { Effect, Context, Layer } from "effect"; import { LinuxUserService, LinuxUser } from "./LinuxUserService"; import { DirectoryService, ClaudeConfig } from "./DirectoryService"; import { QuotaService, DiskQuota } from "./QuotaService"; /** * User Management Service * * High-level orchestration service that coordinates Linux user creation, * directory setup, and resource management for multi-user environments. * * This service integrates: * - LinuxUserService: System user creation and management * - DirectoryService: Home directory and Claude config setup * - QuotaService: Disk quotas and resource limits * * Workflow: * 1. Sanitize and validate username * 2. Create Linux system user * 3. Set up home directory structure * 4. Configure Claude Code environment * 5. Apply resource limits and quotas * 6. Return complete user setup information */ export interface CreateUserRequest { username: string; email: string; claudeApiKey?: string; diskQuotaMB?: number; shell?: "/bin/rbash" | "/usr/sbin/nologin" | "/bin/bash"; } export interface UserSetupResult { linuxUser: LinuxUser; homeDir: string; claudeConfigPath: string; workspacePath: string; quotaApplied: boolean; limitsApplied: boolean; } export interface UserManagementError { readonly _tag: "UserManagementError"; readonly message: string; readonly cause?: unknown; } const UserManagementError = ( message: string, cause?: unknown ): UserManagementError => ({ _tag: "UserManagementError", message, cause, }); export class UserManagementService extends Context.Tag("UserManagementService")< UserManagementService, { /** * Create a complete user environment * - Creates Linux user * - Sets up home directory * - Configures Claude Code * - Applies quotas and limits */ readonly createUser: ( request: CreateUserRequest ) => Effect.Effect; /** * Delete user and clean up all resources */ readonly deleteUser: ( username: string ) => Effect.Effect; /** * Get user information */ readonly getUserInfo: ( username: string ) => Effect.Effect; /** * Update user quotas */ readonly updateQuota: ( username: string, quota: DiskQuota ) => Effect.Effect; /** * Check if username is available */ readonly isUsernameAvailable: ( username: string ) => Effect.Effect; } >() { static Live = Layer.effect( this, Effect.gen(function* () { const linuxUserService = yield* LinuxUserService; const directoryService = yield* DirectoryService; const quotaService = yield* QuotaService; return { createUser: (request: CreateUserRequest) => Effect.gen(function* () { const { username, claudeApiKey, diskQuotaMB = 2048, shell = "/bin/rbash", } = request; // Step 1: Sanitize username for Linux compatibility const linuxUsername = yield* linuxUserService .sanitizeUsername(username) .pipe( Effect.mapError((err) => UserManagementError( `Failed to sanitize username: ${err.message}`, err ) ) ); // Step 2: Check if user already exists const exists = yield* linuxUserService.userExists(linuxUsername).pipe( Effect.mapError((err) => UserManagementError( `Failed to check user existence: ${err.message}`, err ) ) ); if (exists) { return yield* Effect.fail( UserManagementError( `Linux user ${linuxUsername} already exists` ) ); } // Step 3: Create Linux user const linuxUser = yield* linuxUserService .createUser({ username: linuxUsername, shell, createHome: true, groups: ["myaidev"], // Optional: create this group for MyAIDev users }) .pipe( Effect.mapError((err) => UserManagementError( `Failed to create Linux user: ${err.message}`, err ) ) ); // Step 4: Set up home directory structure const claudeConfig: ClaudeConfig = { apiKey: claudeApiKey || "", model: "claude-sonnet-4-20250514", maxTokens: 8192, temperature: 1.0, }; yield* directoryService .setupHomeDirectory({ username: linuxUsername, homeDir: linuxUser.homeDir, uid: linuxUser.uid, gid: linuxUser.gid, claudeConfig, }) .pipe( Effect.mapError((err) => UserManagementError( `Failed to setup home directory: ${err.message}`, err ) ) ); // Step 5: Apply resource limits yield* linuxUserService .setResourceLimits(linuxUsername, { maxOpenFiles: 1024, maxProcesses: 256, maxMemoryKB: 2097152, // 2GB maxCPUTime: 120, // 2 minutes per process }) .pipe( Effect.mapError((err) => UserManagementError( `Failed to set resource limits: ${err.message}`, err ) ) ); // Step 6: Try to apply disk quotas (optional - may not be available) const quotaAvailable = yield* quotaService .isQuotaAvailable() .pipe(Effect.orElseSucceed(() => false)); let quotaApplied = false; if (quotaAvailable) { yield* quotaService .setDiskQuota(linuxUsername, { softLimitMB: diskQuotaMB, hardLimitMB: diskQuotaMB * 1.5, // 50% buffer }) .pipe( Effect.catchAll(() => Effect.succeed(void 0)) // Don't fail if quota setup fails ); quotaApplied = true; } // Return complete setup information return { linuxUser, homeDir: linuxUser.homeDir, claudeConfigPath: `${linuxUser.homeDir}/.claude`, workspacePath: `${linuxUser.homeDir}/workspace`, quotaApplied, limitsApplied: true, }; }), deleteUser: (username: string) => Effect.gen(function* () { // Get user info first const userInfo = yield* linuxUserService.getUserInfo(username).pipe( Effect.mapError((err) => UserManagementError( `Failed to get user info: ${err.message}`, err ) ) ); // Clean up directories yield* directoryService .cleanupDirectories(userInfo.homeDir) .pipe( Effect.mapError((err) => UserManagementError( `Failed to cleanup directories: ${err.message}`, err ) ) ); // Delete Linux user yield* linuxUserService.deleteUser(username, true).pipe( Effect.mapError((err) => UserManagementError( `Failed to delete user: ${err.message}`, err ) ) ); // Remove resource limits file yield* Effect.tryPromise({ try: async () => { const { execSync } = await import("child_process"); execSync(`sudo rm -f /etc/security/limits.d/${username}.conf`, { stdio: "pipe", }); }, catch: () => UserManagementError("Failed to remove limits configuration"), }); }), getUserInfo: (username: string) => linuxUserService.getUserInfo(username).pipe( Effect.mapError((err) => UserManagementError( `Failed to get user info: ${err.message}`, err ) ) ), updateQuota: (username: string, quota: DiskQuota) => quotaService.setDiskQuota(username, quota).pipe( Effect.mapError((err) => UserManagementError( `Failed to update quota: ${err.message}`, err ) ) ), isUsernameAvailable: (username: string) => Effect.gen(function* () { // Sanitize username first const linuxUsername = yield* linuxUserService .sanitizeUsername(username) .pipe( Effect.mapError((err) => UserManagementError( `Failed to sanitize username: ${err.message}`, err ) ) ); // Check if exists const exists = yield* linuxUserService .userExists(linuxUsername) .pipe( Effect.mapError((err) => UserManagementError( `Failed to check availability: ${err.message}`, err ) ) ); return !exists; }), }; }) ); }