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:
co-authored by
Claude Opus 5
parent
92de996412
commit
044aacf4d5
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user