This commit is contained in:
seb
2026-10-06 13:36:03 +02:00
parent cab06d96e8
commit 07f0d5ba80
9 changed files with 1710 additions and 21 deletions

View File

@@ -32,6 +32,10 @@ export function getMimeType(filePath) {
case '.log': return 'text/plain; charset=utf-8';
case '.csv': return 'text/csv; charset=utf-8';
case '.tsv': return 'text/tab-separated-values; charset=utf-8';
case '.xlsx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case '.xls': return 'application/vnd.ms-excel';
case '.xlsm': return 'application/vnd.ms-excel.sheet.macroEnabled.12';
case '.xltx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.template';
case '.md':
case '.skill': return 'text/markdown; charset=utf-8';
case '.xml': return 'application/xml; charset=utf-8';
@@ -68,7 +72,7 @@ export function extractFullHash(p) {
export function isReadOnlyDatabasePath(filePath) {
if (typeof filePath !== 'string' || !filePath.trim()) return false;
const clean = filePath.trim().replace(/^([a-zA-Z]:|\/)+/, '').replace(/^\.\/+/, '');
const clean = filePath.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '').replace(/^\.\/+/, '');
const lower = clean.toLowerCase();
if (lower === 'image' || lower.startsWith('image/')) return true;
if (extractFullHash(clean)) return true;
@@ -186,6 +190,23 @@ export async function prepareVfs({ googleAuthId }) {
try {
const files = await store.modelvfs.list(googleAuthId);
const existingNames = new Set(files.map((f) => f.name));
const defaultSkills = getDefaultSkills();
for (const skill of defaultSkills) {
if (!existingNames.has(skill.name)) {
try {
await store.modelvfs.write(googleAuthId, {
name: skill.name,
mime: 'text/markdown',
content: Buffer.from(skill.content, 'utf8'),
});
files.push({ name: skill.name });
existingNames.add(skill.name);
} catch (err) {
console.warn(`[vfs] failed to write default skill "${skill.name}" to modelvfs:`, err.message);
}
}
}
for (const file of files) {
const diskPath = path.resolve(vfsRoot, file.name);
if (!diskPath.startsWith(vfsRoot)) continue;
@@ -276,10 +297,10 @@ export function createVfs(vfsRoot, options = {}) {
};
const safeResolve = (p) => {
if (typeof p !== 'string' || !p.trim()) {
throw new Error('Path must be a non-empty string');
if (typeof p !== 'string' || !p.trim()) {
throw new Error('Path must be a non-empty string');
}
const cleanVirtual = p.trim().replace(/^([a-zA-Z]:|\/)+/, '');
const cleanVirtual = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '');
const resolved = path.resolve(vfsRoot, cleanVirtual);
const relative = path.relative(vfsRoot, resolved);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
@@ -302,20 +323,44 @@ export function createVfs(vfsRoot, options = {}) {
const vfs = {
readFileSync: (p, opt) => {
const hash = extractFullHash(p);
if (hash) {
const buf = syncFetchImage(hash);
if (buf) {
const encoding = typeof opt === 'string' ? opt : opt?.encoding;
return encoding ? buf.toString(encoding) : buf;
const hash = extractFullHash(p);
if (hash) {
const buf = syncFetchImage(hash);
if (buf) {
const encoding = typeof opt === 'string' ? opt : opt?.encoding;
return encoding ? buf.toString(encoding) : buf;
}
}
const diskPath = safeResolve(p);
try {
}
const diskPath = safeResolve(p);
try {
return fs.readFileSync(diskPath, opt);
} catch (err) {
throw sanitizeFsError(err, p);
}
} catch (err) {
throw sanitizeFsError(err, p);
}
},
readFile: (p, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
const data = vfs.readFileSync(p, options);
if (typeof callback === 'function') process.nextTick(callback, null, data);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
createReadStream: (p, opt) => {
const diskPath = safeResolve(p);
if (!fs.existsSync(diskPath)) {
const err = new Error(`ENOENT: no such file or directory, open '${String(p)}'`);
err.code = 'ENOENT';
err.path = String(p);
err.syscall = 'open';
throw sanitizeFsError(err, p);
}
return fs.createReadStream(diskPath, opt);
},
writeFileSync: (p, data, opt) => {
@@ -329,6 +374,25 @@ export function createVfs(vfsRoot, options = {}) {
}
},
writeFile: (p, data, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
vfs.writeFileSync(p, data, options);
if (typeof callback === 'function') process.nextTick(callback, null);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
createWriteStream: (p, opt) => {
assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p);
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
return fs.createWriteStream(diskPath, opt);
},
appendFileSync: (p, data, opt) => {
assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p);
@@ -436,6 +500,18 @@ export function createVfs(vfsRoot, options = {}) {
assertNotReadOnlyDatabasePath(p);
return safeResolve(p);
},
/**
* Resolve a virtual path to an absolute on-disk path for reading, applying containment checks.
*/
resolveReadPath: (p) => {
return safeResolve(p);
},
/**
* Sanitize on-disk error paths back to virtual paths for display in sandbox transcripts.
*/
sanitizeError: (err, virtualPath) => sanitizeFsError(err, virtualPath),
};
return vfs;