+26
-11
@@ -6,10 +6,21 @@ import { qvacStatus } from '../daemon/qvac-master.js';
|
||||
|
||||
const PERMISSIONS = Object.freeze({ read: 'read', write: 'write', dangerous: 'dangerous', computerUse: 'computer-use' });
|
||||
|
||||
export function filesystemRoots(fsAccess, cwd = process.cwd()) {
|
||||
if (fsAccess === 'filesystem') return ['/'];
|
||||
if (fsAccess === 'home') return [os.homedir()];
|
||||
return [path.resolve(cwd)];
|
||||
}
|
||||
|
||||
function isInside(root, candidate) {
|
||||
const relative = path.relative(root, candidate);
|
||||
return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
||||
}
|
||||
|
||||
// Resolve symlinks as well as lexical paths before accessing workspace files.
|
||||
async function workspacePath(cwd, file, { write = false } = {}) {
|
||||
const root = await realpath(path.resolve(cwd));
|
||||
const target = path.resolve(cwd, file);
|
||||
export async function workspacePath(cwd, file, { write = false, roots } = {}) {
|
||||
const allow = (roots?.length ? roots : [cwd]).map((root) => path.resolve(root));
|
||||
const target = path.isAbsolute(file) ? path.resolve(file) : path.resolve(cwd, file);
|
||||
let resolved;
|
||||
try { resolved = await realpath(target); }
|
||||
catch (error) {
|
||||
@@ -18,8 +29,10 @@ async function workspacePath(cwd, file, { write = false } = {}) {
|
||||
if (entry?.isSymbolicLink()) throw new Error('path is outside the Jarvis workspace or is a dangling symlink');
|
||||
resolved = path.join(await realpath(path.dirname(target)), path.basename(target));
|
||||
}
|
||||
const relative = path.relative(root, resolved);
|
||||
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error('path is outside the Jarvis workspace');
|
||||
const resolvedRoots = await Promise.all(allow.map(async (root) => {
|
||||
try { return await realpath(root); } catch { return root; }
|
||||
}));
|
||||
if (!resolvedRoots.some((root) => isInside(root, resolved))) throw new Error('path is outside the Jarvis workspace');
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -60,7 +73,9 @@ async function searchFiles(root, query, limit = 50) {
|
||||
return hits;
|
||||
}
|
||||
|
||||
export function createPhase2Tools({ cwd = process.cwd(), computer } = {}) {
|
||||
export function createPhase2Tools({ cwd = process.cwd(), computer, roots } = {}) {
|
||||
const allow = roots?.length ? roots : [cwd];
|
||||
const bound = (file, options) => workspacePath(cwd, file, { ...options, roots: allow });
|
||||
return [
|
||||
{
|
||||
name: 'app_list', permission: PERMISSIONS.read,
|
||||
@@ -69,21 +84,21 @@ export function createPhase2Tools({ cwd = process.cwd(), computer } = {}) {
|
||||
},
|
||||
{
|
||||
name: 'fs_search', permission: PERMISSIONS.read,
|
||||
description: 'Search local file and directory names below the Jarvis workspace.',
|
||||
description: 'Search local file and directory names inside the allowed filesystem roots.',
|
||||
parameters: { type: 'object', properties: { query: { type: 'string' }, root: { type: 'string' }, limit: { type: 'number' } }, required: ['query'] },
|
||||
execute: async ({ query, root = cwd, limit = 50 }) => searchFiles(await workspacePath(cwd, root), query, Math.min(100, Number(limit) || 50)),
|
||||
execute: async ({ query, root = cwd, limit = 50 }) => searchFiles(await bound(root), query, Math.min(100, Number(limit) || 50)),
|
||||
},
|
||||
{
|
||||
name: 'fs_read', permission: PERMISSIONS.read,
|
||||
description: 'Read a UTF-8 local text file below the Jarvis workspace, capped at 400 KiB.',
|
||||
description: 'Read a UTF-8 local text file inside the allowed filesystem roots, capped at 400 KiB.',
|
||||
parameters: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] },
|
||||
execute: async ({ file }) => { const target = await workspacePath(cwd, file); return (await readFile(target, 'utf8')).slice(0, 400 * 1024); },
|
||||
execute: async ({ file }) => { const target = await bound(file); return (await readFile(target, 'utf8')).slice(0, 400 * 1024); },
|
||||
},
|
||||
{
|
||||
name: 'fs_write', permission: PERMISSIONS.write,
|
||||
description: 'Write a local text file only after an explicit confirmation flag is supplied.',
|
||||
parameters: { type: 'object', properties: { file: { type: 'string' }, contents: { type: 'string' }, confirmed: { type: 'boolean' } }, required: ['file', 'contents', 'confirmed'] },
|
||||
execute: async ({ file, contents, confirmed }) => { if (confirmed !== true) return { confirmation_required: true, action: 'write', file }; const target = await workspacePath(cwd, file, { write: true }); await writeFile(target, String(contents), 'utf8'); return { ok: true, file: target, bytes: Buffer.byteLength(String(contents)) }; },
|
||||
execute: async ({ file, contents, confirmed }) => { if (confirmed !== true) return { confirmation_required: true, action: 'write', file }; const target = await bound(file, { write: true }); await writeFile(target, String(contents), 'utf8'); return { ok: true, file: target, bytes: Buffer.byteLength(String(contents)) }; },
|
||||
},
|
||||
{
|
||||
name: 'memory_recall', permission: PERMISSIONS.read,
|
||||
|
||||
Reference in New Issue
Block a user