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
@@ -27,8 +27,6 @@ type WSData = {
|
||||
userId: number;
|
||||
email: string;
|
||||
username: string;
|
||||
role: string;
|
||||
sandboxed: boolean;
|
||||
};
|
||||
|
||||
type LiveJob = {
|
||||
@@ -74,9 +72,7 @@ type StartJobParams = {
|
||||
userId: number;
|
||||
email: string;
|
||||
username: string;
|
||||
role: string;
|
||||
mode?: JobMode; // defaults to 'pipeline' for back-compat with the existing pipeline caller
|
||||
sandboxed?: boolean; // script jobs only
|
||||
taskDirName: string;
|
||||
taskName: string;
|
||||
inputs: Record<string, string>;
|
||||
@@ -92,7 +88,10 @@ export function runningCount(): number {
|
||||
|
||||
// Create a job. action 'start' runs it now; 'queue' runs it only if nothing is running, else it stays
|
||||
// 'pending' and gets promoted when the running job finishes. (Single user → one global queue.)
|
||||
export async function enqueueJob(params: StartJobParams, action: 'start' | 'queue'): Promise<{ jobId: string; status: 'running' | 'pending' }> {
|
||||
export async function enqueueJob(
|
||||
params: StartJobParams,
|
||||
action: 'start' | 'queue',
|
||||
): Promise<{ jobId: string; status: 'running' | 'pending' }> {
|
||||
const jobId = randomUUID();
|
||||
const mode: JobMode = params.mode ?? 'pipeline';
|
||||
const run = action === 'start' || runningCount() === 0;
|
||||
@@ -145,7 +144,11 @@ function launch(jobId: string, mode: JobMode, params: StartJobParams) {
|
||||
if (event.type === 'step:complete' || event.type === 'iteration:complete') {
|
||||
const cost = 'cost' in event ? event.cost : undefined;
|
||||
if (cost) {
|
||||
const prev = (job.lastCost as { inputTokens: number; outputTokens: number; totalUSD: number } | null) ?? { inputTokens: 0, outputTokens: 0, totalUSD: 0 };
|
||||
const prev = (job.lastCost as { inputTokens: number; outputTokens: number; totalUSD: number } | null) ?? {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalUSD: 0,
|
||||
};
|
||||
job.lastCost = {
|
||||
inputTokens: prev.inputTokens + cost.inputTokens,
|
||||
outputTokens: prev.outputTokens + cost.outputTokens,
|
||||
@@ -177,8 +180,6 @@ function launch(jobId: string, mode: JobMode, params: StartJobParams) {
|
||||
? executeScript({
|
||||
jobId,
|
||||
email: params.email,
|
||||
role: params.role,
|
||||
sandboxed: params.sandboxed ?? false,
|
||||
taskDirName: params.taskDirName,
|
||||
inputs: params.inputs,
|
||||
cwd: params.cwd,
|
||||
@@ -189,7 +190,6 @@ function launch(jobId: string, mode: JobMode, params: StartJobParams) {
|
||||
userId: params.userId,
|
||||
email: params.email,
|
||||
username: params.username,
|
||||
role: params.role,
|
||||
taskDirName: params.taskDirName,
|
||||
inputs: params.inputs,
|
||||
cwd: params.cwd,
|
||||
@@ -199,36 +199,38 @@ function launch(jobId: string, mode: JobMode, params: StartJobParams) {
|
||||
emit,
|
||||
});
|
||||
|
||||
runner.then(async (result) => {
|
||||
clearInterval(flushInterval);
|
||||
// Script jobs resolve with an exit code — a non-zero exit is a failure. Pipelines resolve void.
|
||||
const exitCode = result && typeof result === 'object' && 'exitCode' in result ? result.exitCode : null;
|
||||
const failed = exitCode !== null && exitCode !== 0;
|
||||
await updatePipelineJob(jobId, {
|
||||
status: failed ? 'failed' : 'completed',
|
||||
exitCode,
|
||||
progress: job.lastProgress as Record<string, unknown>,
|
||||
totalCost: job.lastCost as Record<string, unknown>,
|
||||
error: failed ? `Script exited with code ${exitCode}` : undefined,
|
||||
completedAt: new Date(),
|
||||
}).catch((err) => console.error(`[pipeline-jobs] failed to finalize ${jobId}:`, err));
|
||||
liveJobs.delete(jobId);
|
||||
void promoteNext();
|
||||
}).catch(async (err) => {
|
||||
clearInterval(flushInterval);
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const isStopped = job.abortSignal.aborted;
|
||||
broadcast(job, isStopped ? { type: 'stopped' } : { type: 'error', message });
|
||||
await updatePipelineJob(jobId, {
|
||||
status: isStopped ? 'stopped' : 'failed',
|
||||
progress: job.lastProgress as Record<string, unknown>,
|
||||
totalCost: job.lastCost as Record<string, unknown>,
|
||||
error: isStopped ? undefined : message,
|
||||
completedAt: new Date(),
|
||||
}).catch((e) => console.error(`[pipeline-jobs] failed to finalize ${jobId}:`, e));
|
||||
liveJobs.delete(jobId);
|
||||
void promoteNext();
|
||||
});
|
||||
runner
|
||||
.then(async (result) => {
|
||||
clearInterval(flushInterval);
|
||||
// Script jobs resolve with an exit code — a non-zero exit is a failure. Pipelines resolve void.
|
||||
const exitCode = result && typeof result === 'object' && 'exitCode' in result ? result.exitCode : null;
|
||||
const failed = exitCode !== null && exitCode !== 0;
|
||||
await updatePipelineJob(jobId, {
|
||||
status: failed ? 'failed' : 'completed',
|
||||
exitCode,
|
||||
progress: job.lastProgress as Record<string, unknown>,
|
||||
totalCost: job.lastCost as Record<string, unknown>,
|
||||
error: failed ? `Script exited with code ${exitCode}` : undefined,
|
||||
completedAt: new Date(),
|
||||
}).catch((err) => console.error(`[pipeline-jobs] failed to finalize ${jobId}:`, err));
|
||||
liveJobs.delete(jobId);
|
||||
void promoteNext();
|
||||
})
|
||||
.catch(async (err) => {
|
||||
clearInterval(flushInterval);
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const isStopped = job.abortSignal.aborted;
|
||||
broadcast(job, isStopped ? { type: 'stopped' } : { type: 'error', message });
|
||||
await updatePipelineJob(jobId, {
|
||||
status: isStopped ? 'stopped' : 'failed',
|
||||
progress: job.lastProgress as Record<string, unknown>,
|
||||
totalCost: job.lastCost as Record<string, unknown>,
|
||||
error: isStopped ? undefined : message,
|
||||
completedAt: new Date(),
|
||||
}).catch((e) => console.error(`[pipeline-jobs] failed to finalize ${jobId}:`, e));
|
||||
liveJobs.delete(jobId);
|
||||
void promoteNext();
|
||||
});
|
||||
}
|
||||
|
||||
// When a job finishes (and nothing else is running), promote the oldest queued job. Also called on
|
||||
@@ -239,7 +241,9 @@ async function promoteNext(): Promise<void> {
|
||||
if (!next) return;
|
||||
const user = await getUserById(next.userId);
|
||||
if (!user) {
|
||||
await updatePipelineJob(next.id, { status: 'failed', error: 'user not found', completedAt: new Date() }).catch(() => {});
|
||||
await updatePipelineJob(next.id, { status: 'failed', error: 'user not found', completedAt: new Date() }).catch(
|
||||
() => {},
|
||||
);
|
||||
return promoteNext();
|
||||
}
|
||||
await updatePipelineJob(next.id, { status: 'running', startedAt: new Date() });
|
||||
@@ -248,9 +252,7 @@ async function promoteNext(): Promise<void> {
|
||||
userId: next.userId,
|
||||
email: user.email,
|
||||
username: toShellUsername(user.username ?? '', user.email),
|
||||
role: user.role ?? '',
|
||||
mode: nextMode,
|
||||
sandboxed: (user.role ?? '') !== 'Super Admin',
|
||||
taskDirName: next.taskDirName,
|
||||
taskName: next.taskName,
|
||||
inputs: next.inputs as Record<string, string>,
|
||||
@@ -340,7 +342,9 @@ export async function clearHistory(userId: number): Promise<number> {
|
||||
|
||||
// Lightweight header-badge summary: how many of the user's jobs are running / queued, and which one
|
||||
// is running (for the "running" badge's link).
|
||||
export async function getCounts(userId: number): Promise<{ running: number; runningJobId: string | null; queued: number }> {
|
||||
export async function getCounts(
|
||||
userId: number,
|
||||
): Promise<{ running: number; runningJobId: string | null; queued: number }> {
|
||||
let running = 0;
|
||||
let runningJobId: string | null = null;
|
||||
for (const [id, job] of liveJobs) {
|
||||
@@ -386,7 +390,11 @@ function extractProgress(event: JobEvent, prev: unknown): unknown {
|
||||
return {
|
||||
...p,
|
||||
currentStepIndex: event.stepIndex,
|
||||
parallel: { taskName: event.taskName, concurrency: event.concurrency, iterations: event.iterations.map((l) => ({ label: l, status: 'pending' })) },
|
||||
parallel: {
|
||||
taskName: event.taskName,
|
||||
concurrency: event.concurrency,
|
||||
iterations: event.iterations.map((l) => ({ label: l, status: 'pending' })),
|
||||
},
|
||||
};
|
||||
|
||||
case 'iteration:start':
|
||||
@@ -396,7 +404,7 @@ function extractProgress(event: JobEvent, prev: unknown): unknown {
|
||||
...p,
|
||||
parallel: {
|
||||
...parallel,
|
||||
iterations: parallel.iterations.map((it) => it.label === event.label ? { ...it, status: 'running' } : it),
|
||||
iterations: parallel.iterations.map((it) => (it.label === event.label ? { ...it, status: 'running' } : it)),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -411,7 +419,7 @@ function extractProgress(event: JobEvent, prev: unknown): unknown {
|
||||
...p,
|
||||
parallel: {
|
||||
...parallel,
|
||||
iterations: parallel.iterations.map((it) => it.label === event.label ? { ...it, status } : it),
|
||||
iterations: parallel.iterations.map((it) => (it.label === event.label ? { ...it, status } : it)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user