const fs = require('fs'); const path = require('path'); const { nanoid } = require('nanoid'); // Persist tools to /data (HF persistent storage) when USE_PERSISTENT_STORAGE // is explicitly set. Defaults to the local tools/ directory to avoid hanging // on a slow/broken network mount during startup. function resolveBaseDir() { if (process.env.USE_PERSISTENT_STORAGE) { const candidate = '/data/forge-tools'; try { fs.mkdirSync(candidate, { recursive: true }); fs.accessSync(candidate, fs.constants.W_OK); return candidate; } catch {} } const fallback = path.join(__dirname, '..', 'tools'); fs.mkdirSync(fallback, { recursive: true }); return fallback; } const BASE_DIR = resolveBaseDir(); const MANIFEST_PATH = path.join(BASE_DIR, 'manifest.json'); const usingPersistentStorage = !!(process.env.USE_PERSISTENT_STORAGE && BASE_DIR.startsWith('/data')); function loadManifest() { try { return JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')); } catch { return []; } } function saveManifest(tools) { fs.writeFileSync(MANIFEST_PATH, JSON.stringify(tools, null, 2)); } let tools = loadManifest(); function persist() { saveManifest(tools); } function listTools(status) { return status ? tools.filter((t) => t.status === status) : tools; } function getToolById(id) { return tools.find((t) => t.id === id); } function getToolByName(name) { return tools.find((t) => t.name === name && t.status === 'approved'); } // Tool definitions formatted for the Inference Port chat/completions "tools" param. function getApprovedToolDefs() { return tools .filter((t) => t.status === 'approved') .map((t) => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters && Object.keys(t.parameters).length ? t.parameters : { type: 'object', properties: {} }, }, })); } function proposeTool({ name, description, parameters, code, reason }) { const safeName = String(name).trim().toLowerCase().replace(/[^a-z0-9_]/g, '_').slice(0, 64); const entry = { id: nanoid(10), name: safeName, description: description || '', parameters: parameters || { type: 'object', properties: {} }, code, reason: reason || '', status: 'pending', kind: 'new', version: 1, createdAt: new Date().toISOString(), lastError: null, fixesToolId: null, }; tools.push(entry); persist(); return entry; } // A fix is proposed as its own pending entry linked back to the broken tool. // The original tool stays approved and usable (with lastError noted) until // the fix itself is approved, at which point it replaces the original code. function proposeFix(originalId, newCode, reason) { const original = getToolById(originalId); if (!original) throw new Error(`No tool with id ${originalId}`); const entry = { id: nanoid(10), name: original.name, description: original.description, parameters: original.parameters, code: newCode, reason: reason || 'Automatic fix for a runtime error', status: 'pending', kind: 'fix', version: (original.version || 1) + 1, createdAt: new Date().toISOString(), lastError: null, fixesToolId: originalId, }; tools.push(entry); persist(); return entry; } function approveTool(id) { const entry = getToolById(id); if (!entry) throw new Error(`No tool with id ${id}`); entry.status = 'approved'; if (entry.kind === 'fix' && entry.fixesToolId) { const original = getToolById(entry.fixesToolId); if (original) { original.code = entry.code; original.version = entry.version; original.lastError = null; original.status = 'approved'; // The fix entry itself is folded into the original; drop the duplicate. tools = tools.filter((t) => t.id !== entry.id); persist(); return original; } } persist(); return entry; } function rejectTool(id, reason) { const entry = getToolById(id); if (!entry) throw new Error(`No tool with id ${id}`); entry.status = 'rejected'; entry.rejectReason = reason || ''; if (entry.kind === 'fix') { // Reject the fix but leave the original tool as-is (still broken, still approved // so the agent can decide whether to try a different fix or a different approach). // We snapshot a copy before removing it from the persisted list so the caller // still gets a "rejected" object back for display purposes. const snapshot = { ...entry }; tools = tools.filter((t) => t.id !== entry.id); persist(); return snapshot; } persist(); return entry; } function recordError(id, error) { const entry = getToolById(id); if (!entry) return; entry.lastError = { message: error.message, at: new Date().toISOString() }; persist(); } module.exports = { BASE_DIR, usingPersistentStorage, listTools, getToolById, getToolByName, getApprovedToolDefs, proposeTool, proposeFix, approveTool, rejectTool, recordError, };