ausername-12345
fix: guard /data access behind USE_PERSISTENT_STORAGE env var to prevent startup hang on slow mounts
2a0602e | 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, | |
| }; | |