Add controlled Reasonix local file tools
This commit is contained in:
@@ -23,12 +23,17 @@ import { createInterface } from 'node:readline';
|
|||||||
import { stdin, stdout } from 'node:process';
|
import { stdin, stdout } from 'node:process';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||||
import { readFileSync, existsSync } from 'node:fs';
|
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
|
|
||||||
const MNOTE_WEB_URL = process.env.MNOTE_WEB_URL || 'http://127.0.0.1:3000';
|
const MNOTE_WEB_URL = process.env.MNOTE_WEB_URL || 'http://127.0.0.1:3000';
|
||||||
const DEBUG = process.env.MNOTE_REASONIX_ACP_DEBUG === '1';
|
const DEBUG = process.env.MNOTE_REASONIX_ACP_DEBUG === '1';
|
||||||
|
const REASONIX_ALLOWED_FS_ROOTS = (process.env.REASONIX_ALLOWED_FS_ROOTS || '')
|
||||||
|
.split(':')
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((value) => resolve(value));
|
||||||
|
|
||||||
function debugLog(message) {
|
function debugLog(message) {
|
||||||
if (DEBUG) process.stderr.write(`${message}\n`);
|
if (DEBUG) process.stderr.write(`${message}\n`);
|
||||||
@@ -48,6 +53,64 @@ function stableIdPart(value, fallback) {
|
|||||||
return text || String(fallback || 'unknown');
|
return text || String(fallback || 'unknown');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPathWithinRoot(targetPath, rootPath) {
|
||||||
|
const normalizedRoot = rootPath.endsWith('/') ? rootPath : `${rootPath}/`;
|
||||||
|
const normalizedTarget = targetPath.endsWith('/') ? targetPath : targetPath;
|
||||||
|
return normalizedTarget === rootPath || normalizedTarget.startsWith(normalizedRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertAllowedFsPath(targetPath) {
|
||||||
|
const resolved = resolve(targetPath);
|
||||||
|
if (!REASONIX_ALLOWED_FS_ROOTS.length) {
|
||||||
|
throw new Error(
|
||||||
|
'reasonix local fs tools are disabled; set REASONIX_ALLOWED_FS_ROOTS to enable them',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const matchedRoot = REASONIX_ALLOWED_FS_ROOTS.find((rootPath) => isPathWithinRoot(resolved, rootPath));
|
||||||
|
if (!matchedRoot) {
|
||||||
|
throw new Error(
|
||||||
|
`path not allowed: ${resolved}. allowed roots: ${REASONIX_ALLOWED_FS_ROOTS.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAllowedFsFile(targetPath) {
|
||||||
|
const resolved = assertAllowedFsPath(targetPath);
|
||||||
|
return {
|
||||||
|
path: resolved,
|
||||||
|
content: readFileSync(resolved, 'utf-8'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeAllowedFsFile(targetPath, content) {
|
||||||
|
const resolved = assertAllowedFsPath(targetPath);
|
||||||
|
writeFileSync(resolved, String(content), 'utf-8');
|
||||||
|
return {
|
||||||
|
path: resolved,
|
||||||
|
written: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceAllowedFsFile(targetPath, search, replace) {
|
||||||
|
const resolved = assertAllowedFsPath(targetPath);
|
||||||
|
const current = readFileSync(resolved, 'utf-8');
|
||||||
|
const next = String(current).split(String(search)).join(String(replace));
|
||||||
|
if (next === current) {
|
||||||
|
return {
|
||||||
|
path: resolved,
|
||||||
|
written: false,
|
||||||
|
replaced: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
writeFileSync(resolved, next, 'utf-8');
|
||||||
|
return {
|
||||||
|
path: resolved,
|
||||||
|
written: true,
|
||||||
|
replaced: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildMnoteToolPayload(toolName, rawArgs = {}, context = {}) {
|
function buildMnoteToolPayload(toolName, rawArgs = {}, context = {}) {
|
||||||
const {
|
const {
|
||||||
actorId,
|
actorId,
|
||||||
@@ -383,6 +446,54 @@ tools.register({
|
|||||||
parallelSafe: false,
|
parallelSafe: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (REASONIX_ALLOWED_FS_ROOTS.length) {
|
||||||
|
tools.register({
|
||||||
|
name: 'reasonix_file_read',
|
||||||
|
description: '读取允许根目录中的本地 UTF-8 文件。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
path: { type: 'string', description: '要读取的绝对路径文件' },
|
||||||
|
},
|
||||||
|
required: ['path'],
|
||||||
|
},
|
||||||
|
readOnly: true,
|
||||||
|
fn: async (args) => readAllowedFsFile(args.path),
|
||||||
|
parallelSafe: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
tools.register({
|
||||||
|
name: 'reasonix_file_write',
|
||||||
|
description: '写入允许根目录中的本地 UTF-8 文件。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
path: { type: 'string', description: '要写入的绝对路径文件' },
|
||||||
|
content: { type: 'string', description: '完整文件内容' },
|
||||||
|
},
|
||||||
|
required: ['path', 'content'],
|
||||||
|
},
|
||||||
|
fn: async (args) => writeAllowedFsFile(args.path, args.content),
|
||||||
|
parallelSafe: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
tools.register({
|
||||||
|
name: 'reasonix_file_replace',
|
||||||
|
description: '对允许根目录中的本地 UTF-8 文件做字符串替换。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
path: { type: 'string', description: '要修改的绝对路径文件' },
|
||||||
|
search: { type: 'string', description: '要查找的旧文本' },
|
||||||
|
replace: { type: 'string', description: '要替换成的新文本' },
|
||||||
|
},
|
||||||
|
required: ['path', 'search', 'replace'],
|
||||||
|
},
|
||||||
|
fn: async (args) => replaceAllowedFsFile(args.path, args.search, args.replace),
|
||||||
|
parallelSafe: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Session Store ────────────────────────────────────
|
// ── Session Store ────────────────────────────────────
|
||||||
|
|
||||||
const sessions = new Map();
|
const sessions = new Map();
|
||||||
@@ -414,6 +525,9 @@ onRequest('session/new', async (params) => {
|
|||||||
'Use mnote_doc_fetch to read the current document.',
|
'Use mnote_doc_fetch to read the current document.',
|
||||||
'Use mnote_doc_markdown_edit to apply precise search/replace edits.',
|
'Use mnote_doc_markdown_edit to apply precise search/replace edits.',
|
||||||
'Always use mnote_doc_fetch first to understand the document content before editing.',
|
'Always use mnote_doc_fetch first to understand the document content before editing.',
|
||||||
|
REASONIX_ALLOWED_FS_ROOTS.length
|
||||||
|
? `Local filesystem tools are enabled for these roots: ${REASONIX_ALLOWED_FS_ROOTS.join(', ')}. Use reasonix_file_read, reasonix_file_replace, or reasonix_file_write for files inside those roots.`
|
||||||
|
: 'Local filesystem tools are disabled.',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const loop = new CacheFirstLoop({
|
const loop = new CacheFirstLoop({
|
||||||
|
|||||||
Reference in New Issue
Block a user