This commit is contained in:
seb
2026-11-03 06:20:06 +01:00
parent 24563964a6
commit da785da2dd
21 changed files with 273 additions and 29 deletions

View File

@@ -4,6 +4,7 @@ import multer from 'multer';
import JSZip from 'jszip'; import JSZip from 'jszip';
import env from '../../config/env.js'; import env from '../../config/env.js';
import * as db from '../../db/company.js'; import * as db from '../../db/company.js';
import { requireCompanyAdmin } from '../../db/companyuser.js';
import * as googleAuth from '../../db/googleAuth.js'; import * as googleAuth from '../../db/googleAuth.js';
import { clearCachedUser } from '../middleware/auth.js'; import { clearCachedUser } from '../middleware/auth.js';
import { emit } from '../sse.js'; import { emit } from '../sse.js';
@@ -499,13 +500,25 @@ export function register(app, basePath = '') {
app.post( app.post(
`${basePath}/company/import`, `${basePath}/company/import`,
async (req, res, next) => {
try {
const companyId = requireSelectedCompany(req);
await requireCompanyAdmin(req.user.id, companyId);
next();
} catch (error) {
const status = companyErrorStatus(error);
return res.status(status).json({ error: error.message });
}
},
uploadCompanyImportFile, uploadCompanyImportFile,
async (req, res) => { async (req, res) => {
let companyId; let companyId;
try { try {
companyId = requireSelectedCompany(req); companyId = requireSelectedCompany(req);
await requireCompanyAdmin(req.user.id, companyId);
} catch (error) { } catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message }); const status = companyErrorStatus(error);
return res.status(status).json({ error: error.message });
} }
if (activeImportsByCompany.has(companyId)) { if (activeImportsByCompany.has(companyId)) {
@@ -968,7 +981,7 @@ export function register(app, basePath = '') {
} }
try { try {
const company = await db.updateCompany(parsedId.companyId, body); const company = await db.updateCompany(parsedId.companyId, body, req.user.id);
emit('company', { companyId: parsedId.companyId, data: { companyId: parsedId.companyId } }); emit('company', { companyId: parsedId.companyId, data: { companyId: parsedId.companyId } });
res.status(200).json({ company }); res.status(200).json({ company });
} catch (error) { } catch (error) {
@@ -989,10 +1002,14 @@ export function register(app, basePath = '') {
} }
try { try {
await db.deleteCompany(parsedId.companyId); await db.deleteCompany(parsedId.companyId, req.user.id);
clearCompanyPrinters(parsedId.companyId); clearCompanyPrinters(parsedId.companyId);
res.sendStatus(204); res.sendStatus(204);
} catch (error) { } catch (error) {
const status = companyErrorStatus(error);
if (status !== 500) {
return res.status(status).json({ error: error.message });
}
console.error('Delete company failed:', error); console.error('Delete company failed:', error);
res.status(500).json({ error: 'Failed to delete company' }); res.status(500).json({ error: 'Failed to delete company' });
} }

View File

@@ -2,10 +2,13 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import multer from 'multer'; import multer from 'multer';
import { emit } from '../sse.js'; import { emit } from '../sse.js';
import { requireCompanyAdmin } from '../../db/companyuser.js';
import { import {
prepareVfs, prepareVfs,
resolveVfsRoot, resolveVfsRoot,
isReadOnlyDatabasePath, isReadOnlyDatabasePath,
isProtectedAdminVfsPath,
directoryContainsProtectedAdminVfsFile,
getMimeType, getMimeType,
restoreDefaultSkills, restoreDefaultSkills,
restoreDefaultApplets, restoreDefaultApplets,
@@ -586,6 +589,8 @@ export function register(app, basePath = '') {
: ''; : '';
const uploaded = []; const uploaded = [];
const parsedEntries = [];
let touchesProtected = false;
for (const f of files) { for (const f of files) {
const origName = (f.originalname || 'upload').replace(/[\0\\]/g, '/'); const origName = (f.originalname || 'upload').replace(/[\0\\]/g, '/');
const relPath = targetDir ? `${targetDir}/${origName}` : origName; const relPath = targetDir ? `${targetDir}/${origName}` : origName;
@@ -596,6 +601,18 @@ export function register(app, basePath = '') {
const absPath = path.resolve(vfsRoot, parsed.name); const absPath = path.resolve(vfsRoot, parsed.name);
if (!absPath.startsWith(vfsRoot)) continue; if (!absPath.startsWith(vfsRoot)) continue;
if (isProtectedAdminVfsPath(parsed.name)) {
touchesProtected = true;
}
parsedEntries.push({ f, parsed, absPath });
}
if (touchesProtected) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
for (const { f, parsed, absPath } of parsedEntries) {
fs.mkdirSync(path.dirname(absPath), { recursive: true }); fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, f.buffer); fs.writeFileSync(absPath, f.buffer);
@@ -815,6 +832,15 @@ export function register(app, basePath = '') {
return res.status(404).json({ error: 'Source file or directory not found' }); return res.status(404).json({ error: 'Source file or directory not found' });
} }
if (
isProtectedAdminVfsPath(parsedOld.name) ||
isProtectedAdminVfsPath(parsedNew.name) ||
directoryContainsProtectedAdminVfsFile(absOld) ||
directoryContainsProtectedAdminVfsFile(absNew)
) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
fs.mkdirSync(path.dirname(absNew), { recursive: true }); fs.mkdirSync(path.dirname(absNew), { recursive: true });
fs.renameSync(absOld, absNew); fs.renameSync(absOld, absNew);
@@ -961,6 +987,9 @@ export function register(app, basePath = '') {
} }
try { try {
if (isProtectedAdminVfsPath(parsed.name)) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
const vfsRoot = await resolveVfsRoot(req.user.id); const vfsRoot = await resolveVfsRoot(req.user.id);
const absPath = path.resolve(vfsRoot, parsed.name); const absPath = path.resolve(vfsRoot, parsed.name);
if (!absPath.startsWith(vfsRoot)) { if (!absPath.startsWith(vfsRoot)) {
@@ -1058,6 +1087,9 @@ export function register(app, basePath = '') {
} }
try { try {
if (isProtectedAdminVfsPath(parsed.name)) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
const vfsRoot = await resolveVfsRoot(req.user.id); const vfsRoot = await resolveVfsRoot(req.user.id);
const absPath = path.resolve(vfsRoot, parsed.name); const absPath = path.resolve(vfsRoot, parsed.name);
if (!absPath.startsWith(vfsRoot)) { if (!absPath.startsWith(vfsRoot)) {
@@ -1066,6 +1098,9 @@ export function register(app, basePath = '') {
if (!fs.existsSync(absPath)) { if (!fs.existsSync(absPath)) {
return res.status(404).json({ error: 'File not found' }); return res.status(404).json({ error: 'File not found' });
} }
if (directoryContainsProtectedAdminVfsFile(absPath)) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
const stat = fs.statSync(absPath); const stat = fs.statSync(absPath);
if (stat.isDirectory()) { if (stat.isDirectory()) {
fs.rmSync(absPath, { recursive: true, force: true }); fs.rmSync(absPath, { recursive: true, force: true });

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,11 +7,13 @@ export async function list(googleAuthId) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.account.create(googleAuthId, data); return store.account.create(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.account.delete(googleAuthId, id); return store.account.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId, printeragentId) { export async function list(googleAuthId, printeragentId) {
const store = await getStore(); const store = await getStore();
@@ -16,16 +17,19 @@ export async function findOrCreate(printeragentId, data) {
} }
export async function update(googleAuthId, data) { export async function update(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.agentprinter.update(googleAuthId, data); return store.agentprinter.update(googleAuthId, data);
} }
export async function order(googleAuthId, data) { export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.agentprinter.order(googleAuthId, data); return store.agentprinter.order(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.agentprinter.delete(googleAuthId, id); return store.agentprinter.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.carrier.create(googleAuthId, data); return store.carrier.create(googleAuthId, data);
} }
export async function update(googleAuthId, id, data) { export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.carrier.update(googleAuthId, id, data); return store.carrier.update(googleAuthId, id, data);
} }
export async function order(googleAuthId, data) { export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.carrier.order(googleAuthId, data); return store.carrier.order(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.carrier.delete(googleAuthId, id); return store.carrier.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore, getProvider } from './store.js'; import { getStore, getProvider } from './store.js';
import { requireCompanyAdmin } from './companyuser.js';
export async function listCompanies(authId) { export async function listCompanies(authId) {
const store = await getStore(); const store = await getStore();
@@ -15,12 +16,18 @@ export async function createCompany(googleAuthId, data) {
return store.company.create({ ...data, googleAuthId }); return store.company.create({ ...data, googleAuthId });
} }
export async function updateCompany(id, data) { export async function updateCompany(id, data, googleAuthId) {
if (googleAuthId != null) {
await requireCompanyAdmin(googleAuthId, id);
}
const store = await getStore(); const store = await getStore();
return store.company.update(id, data); return store.company.update(id, data);
} }
export async function deleteCompany(companyId) { export async function deleteCompany(companyId, googleAuthId) {
if (googleAuthId != null) {
await requireCompanyAdmin(googleAuthId, companyId);
}
const store = await getStore(); const store = await getStore();
await store.company.delete(companyId); await store.company.delete(companyId);
} }

View File

@@ -1,4 +1,15 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export const ADMIN_ONLY_SETTING_KEYS = new Set([
'ui.theme',
'keys.tavily',
'keys.openrouter',
'keys.serper',
'modelslugimage',
'systemprompt-prefix',
'systemprompt-suffix',
]);
export async function list(googleAuthId, options) { export async function list(googleAuthId, options) {
const store = await getStore(); const store = await getStore();
@@ -11,6 +22,9 @@ export async function getByKey(googleAuthId, key, options) {
} }
export async function set(googleAuthId, input) { export async function set(googleAuthId, input) {
if (ADMIN_ONLY_SETTING_KEYS.has(input?.key)) {
await requireSelectedCompanyAdmin(googleAuthId);
}
const store = await getStore(); const store = await getStore();
return store.companysetting.set(googleAuthId, input); return store.companysetting.set(googleAuthId, input);
} }

View File

@@ -1,4 +1,53 @@
import { getStore } from './store.js'; import { getStore, getProvider } from './store.js';
export async function resolveCompanyAccess(googleAuthId, companyId) {
const provider = await getProvider();
const access = await provider.query(
`SELECT company_id, role FROM companyuser WHERE company_id = @companyId AND google_auth_id = @googleAuthId`,
{ companyId, googleAuthId }
);
const accessRow = access.rows[0] ?? null;
if (!accessRow) {
const exists = await provider.query(
`SELECT id FROM company WHERE id = @companyId`,
{ companyId }
);
if (!exists.rows[0]) {
throw new Error('Company not found');
}
throw new Error('Not authorized');
}
return {
companyId: accessRow.company_id,
role: accessRow.role ?? 'user',
};
}
export async function resolveSelectedCompanyAccess(googleAuthId) {
const store = await getStore();
const user = await store.google_auth.getById(googleAuthId);
const companyId = user?.selectedCompanyId ?? null;
if (companyId == null) {
throw new Error('No company selected');
}
return resolveCompanyAccess(googleAuthId, companyId);
}
export async function requireCompanyAdmin(googleAuthId, companyId) {
const ctx = await resolveCompanyAccess(googleAuthId, companyId);
if (ctx.role !== 'admin') {
throw new Error('Not authorized');
}
return ctx;
}
export async function requireSelectedCompanyAdmin(googleAuthId) {
const ctx = await resolveSelectedCompanyAccess(googleAuthId);
if (ctx.role !== 'admin') {
throw new Error('Not authorized');
}
return ctx;
}
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,21 +7,25 @@ export async function list(googleAuthId) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.customergroup.create(googleAuthId, data); return store.customergroup.create(googleAuthId, data);
} }
export async function update(googleAuthId, id, data) { export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.customergroup.update(googleAuthId, id, data); return store.customergroup.update(googleAuthId, id, data);
} }
export async function order(googleAuthId, data) { export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.customergroup.order(googleAuthId, data); return store.customergroup.order(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.customergroup.delete(googleAuthId, id); return store.customergroup.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,6 +7,7 @@ export async function list(googleAuthId) {
} }
export async function update(googleAuthId, id, input) { export async function update(googleAuthId, id, input) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.numbers.update(googleAuthId, id, input); return store.numbers.update(googleAuthId, id, input);
} }

View File

@@ -1,4 +1,5 @@
import { getStore, getProvider } from './store.js'; import { getStore, getProvider } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,11 +7,13 @@ export async function list(googleAuthId) {
} }
export async function upsert(googleAuthId, data) { export async function upsert(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.posterminal.upsert(googleAuthId, data); return store.posterminal.upsert(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.posterminal.delete(googleAuthId, id); return store.posterminal.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printer.create(googleAuthId, data); return store.printer.create(googleAuthId, data);
} }
export async function update(googleAuthId, id, data) { export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printer.update(googleAuthId, id, data); return store.printer.update(googleAuthId, id, data);
} }
export async function order(googleAuthId, data) { export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printer.order(googleAuthId, data); return store.printer.order(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printer.delete(googleAuthId, id); return store.printer.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,16 +7,19 @@ export async function list(googleAuthId) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printeragent.create(googleAuthId, data); return store.printeragent.create(googleAuthId, data);
} }
export async function update(googleAuthId, data) { export async function update(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printeragent.update(googleAuthId, data); return store.printeragent.update(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printeragent.delete(googleAuthId, id); return store.printeragent.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.warehouse.create(googleAuthId, data); return store.warehouse.create(googleAuthId, data);
} }
export async function update(googleAuthId, id, data) { export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.warehouse.update(googleAuthId, id, data); return store.warehouse.update(googleAuthId, id, data);
} }
export async function order(googleAuthId, data) { export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.warehouse.order(googleAuthId, data); return store.warehouse.order(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.warehouse.delete(googleAuthId, id); return store.warehouse.delete(googleAuthId, id);
} }

View File

@@ -344,8 +344,8 @@ export async function runSandboxJob(options) {
throw new Error('Sandbox worker pool is shutting down'); throw new Error('Sandbox worker pool is shutting down');
} }
const { poolKey, googleAuthId, code, timeoutMs, dashbackToken, appletInput, appletFile } = options; const { poolKey, googleAuthId, isAdmin = true, code, timeoutMs, dashbackToken, appletInput, appletFile } = options;
const job = { code, timeoutMs, dashbackToken, appletInput, appletFile, poolKey, googleAuthId }; const job = { code, timeoutMs, dashbackToken, appletInput, appletFile, poolKey, googleAuthId, isAdmin };
const entry = await acquireWorker(poolKey, googleAuthId); const entry = await acquireWorker(poolKey, googleAuthId);
if (entry) { if (entry) {

View File

@@ -544,7 +544,7 @@ async function runJob(job) {
const appletInput = job.appletInput; const appletInput = job.appletInput;
const appletFile = job.appletFile; const appletFile = job.appletFile;
const vfs = createVfs(vfsDir, { googleAuthId }); const vfs = createVfs(vfsDir, { googleAuthId, isAdmin: job.isAdmin !== false });
const bridgedExcel = bridgeExcelJS(vfs); const bridgedExcel = bridgeExcelJS(vfs);
const bridgedPDF = bridgePDFKit(vfs); const bridgedPDF = bridgePDFKit(vfs);

View File

@@ -5,6 +5,7 @@ import { defineTool, ToolError } from 'libllmorbiter';
import env from '../config/env.js'; import env from '../config/env.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { createVfs, prepareVfs } from './vfs.js'; import { createVfs, prepareVfs } from './vfs.js';
import { resolveSelectedCompanyAccess } from '../db/companyuser.js';
import { runSandboxJob } from '../services/javascriptSandbox.js'; import { runSandboxJob } from '../services/javascriptSandbox.js';
const DEFAULT_TIMEOUT_MS = 20_000; const DEFAULT_TIMEOUT_MS = 20_000;
@@ -184,11 +185,21 @@ function toTypedCsv(result, columns) {
* *
* @param {{ code: string, timeoutMs: number, vfsRoot: string, googleAuthId: number, appletInput?: object, appletFile?: string }} ctx * @param {{ code: string, timeoutMs: number, vfsRoot: string, googleAuthId: number, appletInput?: object, appletFile?: string }} ctx
*/ */
function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appletFile }) { async function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appletFile }) {
let isAdmin = true;
if (googleAuthId != null) {
try {
const access = await resolveSelectedCompanyAccess(googleAuthId);
isAdmin = access.role === 'admin';
} catch {
isAdmin = false;
}
}
const dashbackToken = jwt.sign({ id: googleAuthId }, env.jwtSecret, { expiresIn: '5m' }); const dashbackToken = jwt.sign({ id: googleAuthId }, env.jwtSecret, { expiresIn: '5m' });
return runSandboxJob({ return runSandboxJob({
poolKey: vfsRoot, poolKey: vfsRoot,
googleAuthId, googleAuthId,
isAdmin,
code, code,
timeoutMs, timeoutMs,
dashbackToken, dashbackToken,

View File

@@ -1,9 +1,9 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { isReadOnlyDatabasePath } from './vfs.js'; import { isReadOnlyDatabasePath, isProtectedAdminVfsPath, prepareVfs } from './vfs.js';
import { requireSelectedCompanyAdmin } from '../db/companyuser.js';
import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js'; import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js';
import { prepareVfs } from './vfs.js';
import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { getStore } from '../db/store.js'; import { getStore } from '../db/store.js';
@@ -155,6 +155,10 @@ export function createPatchTool({ googleAuthId }) {
); );
} }
if (isProtectedAdminVfsPath(clean)) {
await requireSelectedCompanyAdmin(googleAuthId);
}
// A SKILL file must be flat — reject an attempted nested path up front. // A SKILL file must be flat — reject an attempted nested path up front.
const looksSkillByExt = clean.toLowerCase().endsWith(SKILL_EXT.toLowerCase()); const looksSkillByExt = clean.toLowerCase().endsWith(SKILL_EXT.toLowerCase());
if (looksSkillByExt && clean.includes('/')) { if (looksSkillByExt && clean.includes('/')) {

View File

@@ -2,7 +2,8 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { defineTool } from 'libllmorbiter'; import { defineTool } from 'libllmorbiter';
import { isSkillPath } from './skill.js'; import { isSkillPath } from './skill.js';
import { resolveVfsRoot } from './vfs.js'; import { resolveVfsRoot, isProtectedAdminVfsPath, directoryContainsProtectedAdminVfsFile } from './vfs.js';
import { resolveSelectedCompanyAccess, requireSelectedCompanyAdmin } from '../db/companyuser.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
/** /**
@@ -40,6 +41,11 @@ export function createResetVfsTool({ googleAuthId }) {
}, },
}, },
invoke: async ({ include_skills = false } = {}) => { invoke: async ({ include_skills = false } = {}) => {
if (include_skills === true) {
await requireSelectedCompanyAdmin(googleAuthId);
}
const access = await resolveSelectedCompanyAccess(googleAuthId);
const isAdmin = access.role === 'admin';
const keepSkills = include_skills !== true; const keepSkills = include_skills !== true;
const keptSkills = []; const keptSkills = [];
const vfsRoot = await resolveVfsRoot(googleAuthId); const vfsRoot = await resolveVfsRoot(googleAuthId);
@@ -51,8 +57,12 @@ export function createResetVfsTool({ googleAuthId }) {
if (keepSkills && isSkillPath(entry)) { if (keepSkills && isSkillPath(entry)) {
continue; continue;
} }
const fullPath = path.join(vfsRoot, entry);
if (!isAdmin && (isProtectedAdminVfsPath(entry) || directoryContainsProtectedAdminVfsFile(fullPath))) {
continue;
}
try { try {
fs.rmSync(path.join(vfsRoot, entry), { recursive: true, force: true }); fs.rmSync(fullPath, { recursive: true, force: true });
deletedCount += 1; deletedCount += 1;
} catch (err) { } catch (err) {
console.warn(`[reset_vfs] could not remove "${entry}": ${err.message}`); console.warn(`[reset_vfs] could not remove "${entry}": ${err.message}`);

View File

@@ -1,7 +1,8 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { isReadOnlyDatabasePath, prepareVfs } from './vfs.js'; import { isReadOnlyDatabasePath, isProtectedAdminVfsPath, prepareVfs } from './vfs.js';
import { requireSelectedCompanyAdmin } from '../db/companyuser.js';
import { isSkillPath } from './skill.js'; import { isSkillPath } from './skill.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
@@ -94,6 +95,10 @@ export function createRmTool({ googleAuthId }) {
); );
} }
if (isProtectedAdminVfsPath(clean)) {
await requireSelectedCompanyAdmin(googleAuthId);
}
const vfsRoot = await prepareVfs({ googleAuthId }); const vfsRoot = await prepareVfs({ googleAuthId });
const { abs, size } = deleteVfsFile(vfsRoot, clean); const { abs, size } = deleteVfsFile(vfsRoot, clean);

View File

@@ -1,6 +1,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { getStore } from '../db/store.js'; import { getStore } from '../db/store.js';
import { requireSelectedCompanyAdmin } from '../db/companyuser.js';
import { getDefaultSkills } from './skill.js'; import { getDefaultSkills } from './skill.js';
import { getDefaultApplets, DEFAULT_APPLETS_VERSION } from './applet.js'; import { getDefaultApplets, DEFAULT_APPLETS_VERSION } from './applet.js';
@@ -97,6 +98,39 @@ export function isReadOnlyDatabasePath(filePath) {
return lower === 'image' || lower.startsWith('image/') || Boolean(extractFullHash(clean)); return lower === 'image' || lower.startsWith('image/') || Boolean(extractFullHash(clean));
} }
export function isProtectedAdminVfsPath(filePath) {
if (typeof filePath !== 'string' || !filePath.trim()) return false;
const clean = cleanVirtualPath(filePath, true);
const lower = clean.toLowerCase();
return (
lower.endsWith('.skill') ||
lower.endsWith('.applet.js') ||
lower.endsWith('.apple.js') ||
lower === 'apple.js' ||
lower.endsWith('/apple.js')
);
}
export function directoryContainsProtectedAdminVfsFile(absDirPath) {
try {
if (!fs.existsSync(absDirPath)) return false;
const stat = fs.statSync(absDirPath);
if (!stat.isDirectory()) return isProtectedAdminVfsPath(absDirPath);
const entries = fs.readdirSync(absDirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(absDirPath, entry.name);
if (entry.isDirectory()) {
if (directoryContainsProtectedAdminVfsFile(fullPath)) return true;
} else if (isProtectedAdminVfsPath(entry.name)) {
return true;
}
}
return false;
} catch {
return false;
}
}
function assertNotReadOnlyDatabasePath(filePath) { function assertNotReadOnlyDatabasePath(filePath) {
if (isReadOnlyDatabasePath(filePath)) { if (isReadOnlyDatabasePath(filePath)) {
throw new Error( throw new Error(
@@ -190,6 +224,9 @@ export async function loadDefaultSkills(ctx = {}) {
* @returns {Promise<Array<string>>} * @returns {Promise<Array<string>>}
*/ */
export async function restoreDefaultSkills(ctx = {}) { export async function restoreDefaultSkills(ctx = {}) {
if (ctx.googleAuthId != null) {
await requireSelectedCompanyAdmin(ctx.googleAuthId);
}
return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: true, label: 'skill' }); return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: true, label: 'skill' });
} }
@@ -208,6 +245,9 @@ export async function loadDefaultApplets(ctx = {}) {
* @returns {Promise<Array<string>>} * @returns {Promise<Array<string>>}
*/ */
export async function restoreDefaultApplets(ctx = {}) { export async function restoreDefaultApplets(ctx = {}) {
if (ctx.googleAuthId != null) {
await requireSelectedCompanyAdmin(ctx.googleAuthId);
}
return seedVfsFiles(ctx, getVersionedApplets(), { overwrite: true, label: 'applet' }); return seedVfsFiles(ctx, getVersionedApplets(), { overwrite: true, label: 'applet' });
} }
@@ -293,7 +333,14 @@ function cbWrap(syncFn, arityWithOpt = 3) {
* @param {{ googleAuthId: number }} [options] * @param {{ googleAuthId: number }} [options]
*/ */
export function createVfs(vfsRoot, options = {}) { export function createVfs(vfsRoot, options = {}) {
const { googleAuthId } = options; const { googleAuthId, isAdmin = true } = options;
const assertWritablePath = (filePath) => {
assertNotReadOnlyDatabasePath(filePath);
if (!isAdmin && isProtectedAdminVfsPath(filePath)) {
throw new Error('Not authorized');
}
};
// #region Internal Sandbox Helpers // #region Internal Sandbox Helpers
// MARK: - Internal Sandbox Helpers // MARK: - Internal Sandbox Helpers
@@ -405,7 +452,7 @@ export function createVfs(vfsRoot, options = {}) {
// --- Write & Append Operations --- // --- Write & Append Operations ---
writeFileSync: (p, data, opt) => { writeFileSync: (p, data, opt) => {
assertNotReadOnlyDatabasePath(p); assertWritablePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
fs.mkdirSync(path.dirname(diskPath), { recursive: true }); fs.mkdirSync(path.dirname(diskPath), { recursive: true });
try { try {
@@ -416,14 +463,14 @@ export function createVfs(vfsRoot, options = {}) {
}, },
createWriteStream: (p, opt) => { createWriteStream: (p, opt) => {
assertNotReadOnlyDatabasePath(p); assertWritablePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
fs.mkdirSync(path.dirname(diskPath), { recursive: true }); fs.mkdirSync(path.dirname(diskPath), { recursive: true });
return fs.createWriteStream(diskPath, opt); return fs.createWriteStream(diskPath, opt);
}, },
appendFileSync: (p, data, opt) => { appendFileSync: (p, data, opt) => {
assertNotReadOnlyDatabasePath(p); assertWritablePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
fs.mkdirSync(path.dirname(diskPath), { recursive: true }); fs.mkdirSync(path.dirname(diskPath), { recursive: true });
try { try {
@@ -435,7 +482,7 @@ export function createVfs(vfsRoot, options = {}) {
// --- Copy, Rename & Truncate Operations --- // --- Copy, Rename & Truncate Operations ---
copyFileSync: (src, dest, mode) => { copyFileSync: (src, dest, mode) => {
assertNotReadOnlyDatabasePath(dest); assertWritablePath(dest);
const hash = extractFullHash(src); const hash = extractFullHash(src);
if (hash) { if (hash) {
const buf = syncFetchImage(hash); const buf = syncFetchImage(hash);
@@ -452,7 +499,7 @@ export function createVfs(vfsRoot, options = {}) {
}, },
cpSync: (src, dest, opt) => { cpSync: (src, dest, opt) => {
assertNotReadOnlyDatabasePath(dest); assertWritablePath(dest);
const srcDisk = safeResolve(src); const srcDisk = safeResolve(src);
const destDisk = safeResolve(dest); const destDisk = safeResolve(dest);
fs.mkdirSync(path.dirname(destDisk), { recursive: true }); fs.mkdirSync(path.dirname(destDisk), { recursive: true });
@@ -464,10 +511,13 @@ export function createVfs(vfsRoot, options = {}) {
}, },
renameSync: (oldPath, newPath) => { renameSync: (oldPath, newPath) => {
assertNotReadOnlyDatabasePath(oldPath); assertWritablePath(oldPath);
assertNotReadOnlyDatabasePath(newPath); assertWritablePath(newPath);
const oldDisk = safeResolve(oldPath); const oldDisk = safeResolve(oldPath);
const newDisk = safeResolve(newPath); const newDisk = safeResolve(newPath);
if (!isAdmin && (directoryContainsProtectedAdminVfsFile(oldDisk) || directoryContainsProtectedAdminVfsFile(newDisk))) {
throw new Error('Not authorized');
}
fs.mkdirSync(path.dirname(newDisk), { recursive: true }); fs.mkdirSync(path.dirname(newDisk), { recursive: true });
try { try {
return fs.renameSync(oldDisk, newDisk); return fs.renameSync(oldDisk, newDisk);
@@ -477,7 +527,7 @@ export function createVfs(vfsRoot, options = {}) {
}, },
truncateSync: (p, len) => { truncateSync: (p, len) => {
assertNotReadOnlyDatabasePath(p); assertWritablePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
try { try {
return fs.truncateSync(diskPath, len); return fs.truncateSync(diskPath, len);
@@ -600,7 +650,7 @@ export function createVfs(vfsRoot, options = {}) {
}, },
unlinkSync: (p) => { unlinkSync: (p) => {
assertNotReadOnlyDatabasePath(p); assertWritablePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
try { try {
return fs.unlinkSync(diskPath); return fs.unlinkSync(diskPath);
@@ -610,8 +660,11 @@ export function createVfs(vfsRoot, options = {}) {
}, },
rmdirSync: (p, opt) => { rmdirSync: (p, opt) => {
assertNotReadOnlyDatabasePath(p); assertWritablePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) {
throw new Error('Not authorized');
}
try { try {
return fs.rmdirSync(diskPath, opt); return fs.rmdirSync(diskPath, opt);
} catch (err) { } catch (err) {
@@ -620,8 +673,11 @@ export function createVfs(vfsRoot, options = {}) {
}, },
rmSync: (p, opt) => { rmSync: (p, opt) => {
assertNotReadOnlyDatabasePath(p); assertWritablePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) {
throw new Error('Not authorized');
}
try { try {
return fs.rmSync(diskPath, opt); return fs.rmSync(diskPath, opt);
} catch (err) { } catch (err) {
@@ -662,7 +718,7 @@ export function createVfs(vfsRoot, options = {}) {
writeFile: async (p, data, opt) => vfs.writeFileSync(p, data, opt), writeFile: async (p, data, opt) => vfs.writeFileSync(p, data, opt),
appendFile: async (p, data, opt) => vfs.appendFileSync(p, data, opt), appendFile: async (p, data, opt) => vfs.appendFileSync(p, data, opt),
copyFile: async (src, dest, mode) => { copyFile: async (src, dest, mode) => {
assertNotReadOnlyDatabasePath(dest); assertWritablePath(dest);
const hash = extractFullHash(src); const hash = extractFullHash(src);
if (hash) { if (hash) {
const buf = await vfs.promises.readFile(src); const buf = await vfs.promises.readFile(src);
@@ -695,7 +751,7 @@ export function createVfs(vfsRoot, options = {}) {
* containment + read-only guards as `writeFileSync`. * containment + read-only guards as `writeFileSync`.
*/ */
resolveWritePath: (p) => { resolveWritePath: (p) => {
assertNotReadOnlyDatabasePath(p); assertWritablePath(p);
return safeResolve(p); return safeResolve(p);
}, },