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
+30 -31
View File
@@ -2,8 +2,7 @@ import { join, isAbsolute } from 'node:path';
import { mkdirSync, writeFileSync, chmodSync, rmSync, createWriteStream } from 'node:fs';
import { tmpdir } from 'node:os';
import { getTaskByDirName } from './task-files';
import { getHomeDirForRole, DATA_PATH } from '../../data-path';
import { buildSandboxPrefix, buildRunuserSuffix } from '../../sidecar/sandbox';
import { getOwnerHomeDir, DATA_PATH } from '../../data-path';
import { killTree } from './process-tree';
// Script-job event stream. `stdout`/`stderr` are high-frequency (live-only + persisted to the log
@@ -18,8 +17,6 @@ export type ScriptEvent =
export type ExecuteScriptParams = {
jobId: string;
email: string;
role: string;
sandboxed: boolean;
taskDirName: string;
inputs: Record<string, string>;
cwd?: string;
@@ -28,9 +25,21 @@ export type ExecuteScriptParams = {
};
const getRunner = (language: string): string[] =>
language === 'python' ? ['python3'] : language === 'typescript' ? ['bun', 'run'] : language === 'javascript' ? ['node'] : ['bash'];
language === 'python'
? ['python3']
: language === 'typescript'
? ['bun', 'run']
: language === 'javascript'
? ['node']
: ['bash'];
const getFileName = (language: string): string =>
language === 'python' ? 'run.py' : language === 'typescript' ? 'index.ts' : language === 'javascript' ? 'index.js' : 'run.sh';
language === 'python'
? 'run.py'
: language === 'typescript'
? 'index.ts'
: language === 'javascript'
? 'index.js'
: 'run.sh';
function materializeScript(language: string, implementation: string): string {
const dir = join(tmpdir(), `officer-task-${Date.now()}-${Math.random().toString(36).slice(2)}`);
@@ -57,7 +66,7 @@ export const jobLogPath = (jobId: string) => join(DATA_PATH, 'jobs', `${jobId}.l
// output to a durable log file. Resolves with the process exit code; throws only on spawn failure or
// when aborted (the manager maps those to failed/stopped).
export async function executeScript(params: ExecuteScriptParams): Promise<{ exitCode: number }> {
const { jobId, email, role, sandboxed, inputs, abortSignal, emit } = params;
const { jobId, email, inputs, abortSignal, emit } = params;
const task = await getTaskByDirName(params.taskDirName);
if (!task) throw new Error(`Task not found: ${params.taskDirName}`);
@@ -70,35 +79,21 @@ export async function executeScript(params: ExecuteScriptParams): Promise<{ exit
const positionalArgs = buildArgs(inputs, task.args);
const cmd = [...getRunner(language), scriptPath, ...positionalArgs];
const homeDir = getHomeDirForRole(email, role);
const homeDir = getOwnerHomeDir(email);
const cwd = params.cwd ? (isAbsolute(params.cwd) ? params.cwd : join(homeDir, params.cwd)) : homeDir;
let spawnCmd: string[];
let spawnEnv: Record<string, string>;
let spawnCwd: string;
if (sandboxed) {
const prefix = buildSandboxPrefix(email);
const suffix = buildRunuserSuffix();
const userDataPrefix = join(DATA_PATH, email);
const translatePath = (v: string) => (v.startsWith(userDataPrefix) ? '/data' + v.slice(userDataPrefix.length) : v);
const envArgs: string[] = [];
for (const [key, value] of Object.entries(inputEnv)) envArgs.push('--setenv', key, translatePath(value));
const sandboxCmd = cmd.map((arg) => translatePath(arg));
const scriptDir = join(scriptPath, '..');
spawnCmd = [...prefix, '--ro-bind', scriptDir, scriptDir, ...envArgs, ...suffix, ...sandboxCmd];
spawnEnv = {};
spawnCwd = '/';
} else {
spawnCmd = cmd;
spawnEnv = { ...(process.env as Record<string, string>), ...inputEnv };
spawnCwd = cwd;
}
const spawnCmd = cmd;
const spawnEnv = { ...(process.env as Record<string, string>), ...inputEnv };
const spawnCwd = cwd;
mkdirSync(join(DATA_PATH, 'jobs'), { recursive: true });
const log = createWriteStream(jobLogPath(jobId), { flags: 'w' });
const cleanup = () => {
try { rmSync(join(scriptPath, '..'), { recursive: true, force: true }); } catch { /* best effort */ }
try {
rmSync(join(scriptPath, '..'), { recursive: true, force: true });
} catch {
/* best effort */
}
};
emit({ type: 'started', taskName: task.name });
@@ -109,7 +104,11 @@ export async function executeScript(params: ExecuteScriptParams): Promise<{ exit
const abortPoll = setInterval(() => {
if (abortSignal.aborted) {
clearInterval(abortPoll);
try { killTree(proc.pid); } catch { /* already dead */ }
try {
killTree(proc.pid);
} catch {
/* already dead */
}
}
}, 500);