Check Point
Rolling release / release (push) Successful in 6m36s

This commit is contained in:
2026-09-12 09:07:09 -04:00
parent e9040d110a
commit a4073b9020
63 changed files with 2459 additions and 632 deletions
+21 -4
View File
@@ -1,4 +1,4 @@
import { readdir, readFile, writeFile } from 'node:fs/promises';
import { readdir, readFile, writeFile, realpath, lstat } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { SKILLS } from './catalog.js';
@@ -6,6 +6,23 @@ import { qvacStatus } from '../daemon/qvac-master.js';
const PERMISSIONS = Object.freeze({ read: 'read', write: 'write', dangerous: 'dangerous', computerUse: 'computer-use' });
// 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);
let resolved;
try { resolved = await realpath(target); }
catch (error) {
if (!write || error.code !== 'ENOENT') throw error;
const entry = await lstat(target).catch((cause) => { if (cause.code !== 'ENOENT') throw cause; return null; });
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');
return resolved;
}
async function desktopApps() {
const dirs = ['/usr/share/applications', path.join(os.homedir(), '.local/share/applications')];
const apps = [];
@@ -54,19 +71,19 @@ export function createPhase2Tools({ cwd = process.cwd(), computer } = {}) {
name: 'fs_search', permission: PERMISSIONS.read,
description: 'Search local file and directory names below the Jarvis workspace.',
parameters: { type: 'object', properties: { query: { type: 'string' }, root: { type: 'string' }, limit: { type: 'number' } }, required: ['query'] },
execute: ({ query, root = cwd, limit = 50 }) => searchFiles(root, query, Math.min(100, Number(limit) || 50)),
execute: async ({ query, root = cwd, limit = 50 }) => searchFiles(await workspacePath(cwd, 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.',
parameters: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] },
execute: async ({ file }) => { const target = path.resolve(cwd, file); if (!target.startsWith(`${path.resolve(cwd)}${path.sep}`)) throw new Error('path is outside the Jarvis workspace'); return (await readFile(target, 'utf8')).slice(0, 400 * 1024); },
execute: async ({ file }) => { const target = await workspacePath(cwd, 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 = path.resolve(cwd, file); if (!target.startsWith(`${path.resolve(cwd)}${path.sep}`)) throw new Error('path is outside the Jarvis workspace'); await writeFile(target, String(contents), 'utf8'); return { ok: true, file: target, bytes: String(contents).length }; },
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)) }; },
},
{
name: 'memory_recall', permission: PERMISSIONS.read,