u
This commit is contained in:
@@ -48,7 +48,7 @@ Applets run in an isolated worker thread pool sandbox.
|
||||
- **No `require()` or `import`**: Attempting to import modules throws an error.
|
||||
- **Pre-injected Globals**:
|
||||
- `fetch(path, options)`: Authenticated `fetch` pre-configured with a JWT scoped to the current user and selected company. Use root-relative API routes, e.g. `fetch('/article/42000009')`.
|
||||
- `fs` / `vfs`: Sandboxed VFS filesystem interface for reading/writing local company files.
|
||||
- `fs`: Sandboxed filesystem interface for reading/writing local company files.
|
||||
- `ExcelJS`: Pre-injected spreadsheet library.
|
||||
- `PDFDocument`: Pre-injected PDFKit library.
|
||||
- `sharp`: Pre-injected image manipulation library.
|
||||
|
||||
@@ -600,7 +600,6 @@ async function runJob(job) {
|
||||
sandbox: {
|
||||
console: sandboxConsole,
|
||||
fs: vfs,
|
||||
vfs,
|
||||
sharp: bridgeSharp(vfs),
|
||||
ExcelJS: bridgedExcel,
|
||||
exceljs: bridgedExcel,
|
||||
|
||||
@@ -49,7 +49,7 @@ export const TOOL_DESCRIPTIONS = {
|
||||
javascript_execute:
|
||||
'Execute JS in an isolated sandbox via inline `code` (assign to `module.exports`) or `appletvfsfile` ' +
|
||||
'(.applet.js returning typed CSV per @applet-input-schema, @applet-output-schema, and @applet-description; details in applet.SKILL). ' +
|
||||
'Pre-injected globals: fs (vfs; use only root-relative paths for all file operations, absolute paths will fail), sharp, ExcelJS, PDFDocument, crypto, and fetch("/path"). No require/import.',
|
||||
'Pre-injected globals: fs (use only root-relative paths for all file operations, absolute paths will fail), sharp, ExcelJS, PDFDocument, crypto, and fetch("/path"). No require/import.',
|
||||
|
||||
find_files:
|
||||
'List VFS files (root-relative paths). Set skills=true to list root-level "<slug>.SKILL" files.',
|
||||
|
||||
345
src/tools/vfs.js
345
src/tools/vfs.js
@@ -435,6 +435,193 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
appendFile: (p, data, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
vfs.appendFileSync(p, data, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
copyFileSync: (src, dest, mode) => {
|
||||
assertNotReadOnlyDatabasePath(dest);
|
||||
const hash = extractFullHash(src);
|
||||
if (hash) {
|
||||
const buf = syncFetchImage(hash);
|
||||
if (buf) {
|
||||
const destDisk = safeResolve(dest);
|
||||
fs.mkdirSync(path.dirname(destDisk), { recursive: true });
|
||||
if (typeof mode === 'number' && (mode & fs.constants.COPYFILE_EXCL) && fs.existsSync(destDisk)) {
|
||||
const err = new Error(`EEXIST: file already exists, copyfile '${src}' -> '${dest}'`);
|
||||
err.code = 'EEXIST';
|
||||
err.path = dest;
|
||||
err.syscall = 'copyfile';
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
return fs.writeFileSync(destDisk, buf);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
const srcDisk = safeResolve(src);
|
||||
const destDisk = safeResolve(dest);
|
||||
fs.mkdirSync(path.dirname(destDisk), { recursive: true });
|
||||
try {
|
||||
return fs.copyFileSync(srcDisk, destDisk, mode);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, `${src} -> ${dest}`);
|
||||
}
|
||||
},
|
||||
|
||||
copyFile: (src, dest, mode, cb) => {
|
||||
const callback = typeof mode === 'function' ? mode : cb;
|
||||
const m = typeof mode === 'function' ? undefined : mode;
|
||||
try {
|
||||
vfs.copyFileSync(src, dest, m);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
cpSync: (src, dest, opt) => {
|
||||
assertNotReadOnlyDatabasePath(dest);
|
||||
const srcDisk = safeResolve(src);
|
||||
const destDisk = safeResolve(dest);
|
||||
fs.mkdirSync(path.dirname(destDisk), { recursive: true });
|
||||
try {
|
||||
return fs.cpSync(srcDisk, destDisk, opt);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, `${src} -> ${dest}`);
|
||||
}
|
||||
},
|
||||
|
||||
cp: (src, dest, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
vfs.cpSync(src, dest, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
renameSync: (oldPath, newPath) => {
|
||||
assertNotReadOnlyDatabasePath(oldPath);
|
||||
assertNotReadOnlyDatabasePath(newPath);
|
||||
const oldDisk = safeResolve(oldPath);
|
||||
const newDisk = safeResolve(newPath);
|
||||
fs.mkdirSync(path.dirname(newDisk), { recursive: true });
|
||||
try {
|
||||
return fs.renameSync(oldDisk, newDisk);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, `${oldPath} -> ${newPath}`);
|
||||
}
|
||||
},
|
||||
|
||||
rename: (oldPath, newPath, cb) => {
|
||||
try {
|
||||
vfs.renameSync(oldPath, newPath);
|
||||
if (typeof cb === 'function') process.nextTick(cb, null);
|
||||
} catch (err) {
|
||||
if (typeof cb === 'function') process.nextTick(cb, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
truncateSync: (p, len) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
try {
|
||||
return fs.truncateSync(diskPath, len);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
truncate: (p, len, cb) => {
|
||||
const callback = typeof len === 'function' ? len : cb;
|
||||
const l = typeof len === 'function' ? undefined : len;
|
||||
try {
|
||||
vfs.truncateSync(p, l);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
accessSync: (p, mode) => {
|
||||
const hash = extractFullHash(p);
|
||||
if (hash) {
|
||||
const buf = syncFetchImage(hash);
|
||||
if (buf) {
|
||||
if (typeof mode === 'number' && (mode & fs.constants.W_OK)) {
|
||||
const err = new Error(`EACCES: permission denied, access '${p}'`);
|
||||
err.code = 'EACCES';
|
||||
err.path = p;
|
||||
err.syscall = 'access';
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
const diskPath = safeResolve(p);
|
||||
try {
|
||||
return fs.accessSync(diskPath, mode);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
access: (p, mode, cb) => {
|
||||
const callback = typeof mode === 'function' ? mode : cb;
|
||||
const m = typeof mode === 'function' ? undefined : mode;
|
||||
try {
|
||||
vfs.accessSync(p, m);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
realpathSync: (p, opt) => {
|
||||
const diskPath = safeResolve(p);
|
||||
try {
|
||||
const resolved = fs.realpathSync(diskPath, opt);
|
||||
const rel = path.relative(vfsRoot, resolved);
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
throw new Error(`Access denied: path "${p}" escapes virtual filesystem boundary.`);
|
||||
}
|
||||
return rel ? rel.split(path.sep).join('/') : '.';
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
realpath: (p, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
const res = vfs.realpathSync(p, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null, res);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
existsSync: (p) => {
|
||||
const hash = extractFullHash(p);
|
||||
if (hash) {
|
||||
@@ -449,6 +636,11 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
exists: (p, cb) => {
|
||||
const res = vfs.existsSync(p);
|
||||
if (typeof cb === 'function') process.nextTick(cb, res);
|
||||
},
|
||||
|
||||
statSync: (p, opt) => {
|
||||
const hash = extractFullHash(p);
|
||||
if (hash) {
|
||||
@@ -466,18 +658,63 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
}
|
||||
}
|
||||
const diskPath = safeResolve(p);
|
||||
return fs.statSync(diskPath, opt);
|
||||
try {
|
||||
return fs.statSync(diskPath, opt);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
stat: (p, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
const res = vfs.statSync(p, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null, res);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
lstatSync: (p, opt) => vfs.statSync(p, opt),
|
||||
|
||||
lstat: (p, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
const res = vfs.lstatSync(p, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null, res);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
readdirSync: (p = '.', opt) => {
|
||||
const clean = typeof p === 'string' ? p.trim().replace(/^([a-zA-Z]:|\/)+/, '') : '';
|
||||
if (VIRTUAL_IMAGE_FOLDERS.has(clean.toLowerCase())) {
|
||||
return [];
|
||||
}
|
||||
const diskPath = safeResolve(p);
|
||||
return fs.readdirSync(diskPath, opt);
|
||||
try {
|
||||
return fs.readdirSync(diskPath, opt);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
readdir: (p = '.', opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : (typeof p === 'function' ? p : cb);
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
const pathArg = typeof p === 'function' ? '.' : p;
|
||||
try {
|
||||
const res = vfs.readdirSync(pathArg, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null, res);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
mkdirSync: (p, opt) => {
|
||||
@@ -489,22 +726,81 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
mkdir: (p, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
const res = vfs.mkdirSync(p, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null, res);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
unlinkSync: (p) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
return fs.unlinkSync(diskPath);
|
||||
try {
|
||||
return fs.unlinkSync(diskPath);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
unlink: (p, cb) => {
|
||||
try {
|
||||
vfs.unlinkSync(p);
|
||||
if (typeof cb === 'function') process.nextTick(cb, null);
|
||||
} catch (err) {
|
||||
if (typeof cb === 'function') process.nextTick(cb, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
rmdirSync: (p, opt) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
return fs.rmdirSync(diskPath, opt);
|
||||
try {
|
||||
return fs.rmdirSync(diskPath, opt);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
rmdir: (p, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
vfs.rmdirSync(p, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
rmSync: (p, opt) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
return fs.rmSync(diskPath, opt);
|
||||
try {
|
||||
return fs.rmSync(diskPath, opt);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
rm: (p, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
vfs.rmSync(p, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
promises: {
|
||||
@@ -538,11 +834,41 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
},
|
||||
writeFile: async (p, data, opt) => vfs.writeFileSync(p, data, opt),
|
||||
appendFile: async (p, data, opt) => vfs.appendFileSync(p, data, opt),
|
||||
copyFile: async (src, dest, mode) => {
|
||||
assertNotReadOnlyDatabasePath(dest);
|
||||
const hash = extractFullHash(src);
|
||||
if (hash) {
|
||||
const buf = await vfs.promises.readFile(src);
|
||||
const destDisk = safeResolve(dest);
|
||||
fs.mkdirSync(path.dirname(destDisk), { recursive: true });
|
||||
if (typeof mode === 'number' && (mode & fs.constants.COPYFILE_EXCL) && fs.existsSync(destDisk)) {
|
||||
const err = new Error(`EEXIST: file already exists, copyfile '${src}' -> '${dest}'`);
|
||||
err.code = 'EEXIST';
|
||||
err.path = dest;
|
||||
err.syscall = 'copyfile';
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
return fs.writeFileSync(destDisk, buf);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, dest);
|
||||
}
|
||||
}
|
||||
return vfs.copyFileSync(src, dest, mode);
|
||||
},
|
||||
cp: async (src, dest, opt) => vfs.cpSync(src, dest, opt),
|
||||
rename: async (oldPath, newPath) => vfs.renameSync(oldPath, newPath),
|
||||
truncate: async (p, len) => vfs.truncateSync(p, len),
|
||||
access: async (p, mode) => vfs.accessSync(p, mode),
|
||||
realpath: async (p, opt) => vfs.realpathSync(p, opt),
|
||||
stat: async (p, opt) => vfs.statSync(p, opt),
|
||||
lstat: async (p, opt) => vfs.lstatSync(p, opt),
|
||||
readdir: async (p, opt) => vfs.readdirSync(p, opt),
|
||||
mkdir: async (p, opt) => vfs.mkdirSync(p, opt),
|
||||
mkdir: async (p, opt) => vfs.mkdirSync(p, opt),
|
||||
rmdir: async (p, opt) => vfs.rmdirSync(p, opt),
|
||||
unlink: async (p) => vfs.unlinkSync(p),
|
||||
rm: async (p, opt) => vfs.rmSync(p, opt),
|
||||
rm: async (p, opt) => vfs.rmSync(p, opt),
|
||||
constants: fs.constants,
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -573,5 +899,10 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
sanitizeError: (err, virtualPath) => sanitizeFsError(err, virtualPath),
|
||||
};
|
||||
|
||||
vfs.realpathSync.native = (p, opt) => vfs.realpathSync(p, opt);
|
||||
vfs.realpath.native = (p, opt, cb) => vfs.realpath(p, opt, cb);
|
||||
vfs.promises.realpath.native = async (p, opt) => vfs.realpathSync(p, opt);
|
||||
vfs.constants = fs.constants;
|
||||
|
||||
return vfs;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user