remove the dead multi-user surface

Officer is single-user: the server owner is the only account, created once by
/auth/bootstrap. Everything that existed to serve additional users was
unreachable, so it is gone rather than left looking like it does something.

Accounts: drop the invite / resend-invite / delete / list-users routes and the
Users settings screen, the inert /auth/signup handler, and the account
verification chain it fed (verify, resend-verification, VerifyScreen, the
UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token
survives for password resets only, and now requires a reset-password token
rather than accepting any signed JWT.

Roles: drop the users.role column and the four-value USER_ROLES enum. The
permissions table granted every role identical methods, and every
role === 'Super Admin' check was permanently true. The JWT no longer carries a
role claim.

Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected
only for non-Super-Admin users, so it never ran. It was also not a usable agent
jail as written — --share-net, the project root (with .env) bound read-only,
and runuser dropping to the server's own uid. Rebuilding it for agent
containment would be a different construction, and git history keeps this one.

getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the
owner's real login home, which is what terminals, chats and task runs use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
brunorezio
2026-07-25 23:30:20 +01:00
co-authored by Claude Opus 5
parent 92de996412
commit 044aacf4d5
85 changed files with 2761 additions and 2121 deletions
+10 -24
View File
@@ -7,7 +7,7 @@ import { sendOpenCodeStreaming } from '@@/channels/send-opencode';
import { ensureGeneralChatSessionsCwd } from './claude-sessions';
import * as sidecar from '@@/sidecar-registry';
import { join } from 'path';
import { getHomeDirForRole, getEmailAccountsDir } from '../../../servers/data-path';
import { getOwnerHomeDir, getEmailAccountsDir } from '../../../servers/data-path';
import { getUserSettings, getEmailAccounts } from 'officerdb';
import { mkdirSync } from 'node:fs';
import { logger } from './logger';
@@ -34,28 +34,21 @@ type WSData = {
userId: number;
email: string;
username: string;
role: string;
sandboxed: boolean;
provider: string;
};
const IDLE_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour
const resolveCwd = (email: string, role: string, cwd?: string) => {
const root = getHomeDirForRole(email, role);
const resolveCwd = (email: string, cwd?: string) => {
const root = getOwnerHomeDir(email);
if (!cwd || cwd === '~') return root;
if (cwd.startsWith('~/')) return join(root, cwd.slice(2));
if (cwd.startsWith('/')) {
// Super Admin: trust absolute paths as-is
if (role === 'Super Admin') return cwd;
return join(root, cwd.slice(1));
}
// The server owner is the only account — absolute paths are theirs to use.
if (cwd.startsWith('/')) return cwd;
return join(root, cwd);
};
export const resolveBaseCwd = (email: string, role: string, cwd?: string) => {
return resolveCwd(email, role, cwd);
};
export const resolveBaseCwd = (email: string, cwd?: string) => resolveCwd(email, cwd);
// The email chat runs from the selected account's storage dir:
// DATA_PATH/<owner>/email_accounts/<accountEmail>
@@ -81,13 +74,11 @@ async function resolveEmailCwd(userId: number, ownerEmail: string, accountEmail?
async function resolveChatCwd(
msg: { context?: string; contextId?: string; cwd?: string },
email: string,
role: string,
userId: number,
): Promise<string> {
if (msg.context === 'email') return resolveEmailCwd(userId, email, msg.contextId);
if (msg.context === 'chat')
return msg.cwd?.trim() ? resolveCwd(email, role, msg.cwd) : ensureGeneralChatSessionsCwd(email);
return resolveCwd(email, role, msg.cwd);
if (msg.context === 'chat') return msg.cwd?.trim() ? resolveCwd(email, msg.cwd) : ensureGeneralChatSessionsCwd(email);
return resolveCwd(email, msg.cwd);
}
const wsToSessionMap = new WeakMap<any, string>();
@@ -295,7 +286,6 @@ async function handleChat(
model?: string;
cwd?: string;
cwdRoot?: string;
sandboxed?: boolean;
groupSlug?: string;
attachmentIds?: string[];
thinking?: string;
@@ -336,14 +326,13 @@ async function handleClaudeCodeChat(
contextId?: string;
cwd?: string;
cwdRoot?: string;
sandboxed?: boolean;
resumeSessionId?: string;
},
effectivePrompt: string,
): Promise<void> {
const { email, username, userId } = ws.data;
const cwd = await resolveChatCwd(msg, email, ws.data.role, userId);
const cwd = await resolveChatCwd(msg, email, userId);
const groupSlug = msg.groupSlug || null;
@@ -389,7 +378,6 @@ async function handleClaudeCodeChat(
sessionKey: sessionId,
cwd,
model,
role: ws.data.role,
resumeSessionId: msg.resumeSessionId,
onEvent,
});
@@ -416,14 +404,13 @@ async function handleOpenCodeChat(
contextId?: string;
cwd?: string;
cwdRoot?: string;
sandboxed?: boolean;
resumeSessionId?: string;
},
effectivePrompt: string,
): Promise<void> {
const { email, username, userId } = ws.data;
const cwd = await resolveChatCwd(msg, email, ws.data.role, userId);
const cwd = await resolveChatCwd(msg, email, userId);
const groupSlug = msg.groupSlug || null;
@@ -468,7 +455,6 @@ async function handleOpenCodeChat(
sessionKey: sessionId,
cwd,
model,
role: ws.data.role,
resumeSessionId: msg.resumeSessionId,
onEvent,
});