@justinechang39/maki
Version:
AI-powered CLI agent for file operations, CSV manipulation, todo management, and web content fetching using OpenRouter
89 lines (88 loc) • 3.09 kB
JavaScript
import path from 'path';
import { WORKSPACE_DIRECTORY, WORKSPACE_DIRECTORY_NAME } from './config.js';
/**
* Checks if a URL is safe to fetch (public, http/https, not local/private).
*/
export function isSafeUrl(urlString) {
try {
const parsedUrl = new URL(urlString);
const hostname = parsedUrl.hostname;
// 1. Protocol check (allow http and https)
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
return false;
}
// 2. Disallow localhost and loopback
if (hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '[::1]') {
return false;
}
// 3. Disallow private IP ranges (IPv4 and IPv6 ULA)
if (/^10\./.test(hostname) ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname) ||
/^192\.168\./.test(hostname) ||
/^fd[0-9a-f]{2}:/i.test(hostname)) {
return false;
}
// 4. Disallow .local TLD (often used for mDNS/Bonjour on local networks)
if (hostname.endsWith('.local')) {
return false;
}
return true;
}
catch (error) {
return false;
}
}
/**
* Resolves a user-provided path against the workspace directory and ensures it's safe.
*/
export function getSafeWorkspacePath(userPath = '.') {
const resolvedPath = path.resolve(WORKSPACE_DIRECTORY, userPath);
if (!resolvedPath.startsWith(WORKSPACE_DIRECTORY)) {
throw new Error(`Path traversal attempt detected. Path must be within '${WORKSPACE_DIRECTORY_NAME}'.`);
}
return resolvedPath;
}
/**
* Validates conversation history, primarily for dangling tool calls.
*/
export function validateConversationHistory(messages) {
const toolCallIds = new Set();
const toolResponseIds = new Set();
for (const message of messages) {
if (message.role === 'assistant' && message.tool_calls) {
message.tool_calls.forEach(call => toolCallIds.add(call.id));
}
if (message.role === 'tool' && message.tool_call_id) {
toolResponseIds.add(message.tool_call_id);
}
}
const unansweredCalls = new Set();
for (const id of toolCallIds) {
if (!toolResponseIds.has(id)) {
unansweredCalls.add(id);
}
}
if (unansweredCalls.size === 0) {
return messages;
}
let lastValidIndex = messages.length;
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.role === 'assistant' && message.tool_calls) {
const hasUnanswered = message.tool_calls.some(call => unansweredCalls.has(call.id));
if (hasUnanswered) {
lastValidIndex = i;
break;
}
}
}
if (lastValidIndex < messages.length) {
if (lastValidIndex > 0 && messages[lastValidIndex - 1].role === 'user') {
return messages.slice(0, lastValidIndex - 1);
}
return messages.slice(0, lastValidIndex);
}
return messages;
}