Forge / src /toolManager.js
ausername-12345
fix: guard /data access behind USE_PERSISTENT_STORAGE env var to prevent startup hang on slow mounts
2a0602e
Raw
History Blame Contribute Delete
5.06 kB
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,
};