/** * Page-registered custom tools (schemas only; handlers stay in the page). * No Bare imports — unit-testable on Node. */ const toolSet = require('./tool-set.js'); const NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/; const MAX_TOOLS = 32; const MAX_DESC = 2000; const bySession = new Map(); const sessionOpts = new Map(); const handlers = new Map(); function setReserved(names) { reserved = new Set(names); } let reserved = new Set(toolSet.ALWAYS_RESERVED.concat(toolSet.ALWAYS_BUILTIN_RESERVED)); function setSession(sessionId, opts) { if (!sessionId) return; sessionOpts.set(sessionId, { hostWorkspace: toolSet.parseHostWorkspace(opts), }); } function isReserved(name, sessionId) { if (reserved.has(name)) return true; if (!toolSet.isHostWorkspaceTool(name)) return false; const flags = sessionId ? sessionOpts.get(sessionId) : null; if (flags && flags.hostWorkspace === false) return false; return true; } function normalizeSchema(raw, sessionId) { if (!raw || typeof raw !== 'object') throw new Error('tool schema required'); let name = raw.name; let description = raw.description; let parameters = raw.parameters; if (raw.type === 'function' && raw.function) { name = raw.function.name; description = raw.function.description; parameters = raw.function.parameters; } if (!NAME_RE.test(String(name || ''))) throw new Error('invalid tool name'); if (isReserved(name, sessionId)) throw new Error('tool name is reserved: ' + name); const desc = String(description || '').slice(0, MAX_DESC); let params = parameters && typeof parameters === 'object' ? parameters : { type: 'object', properties: {} }; if (params.type && params.type !== 'object') { throw new Error('tool parameters must be a JSON object schema'); } return { type: 'function', name, description: desc || name, parameters: { type: 'object', properties: params.properties && typeof params.properties === 'object' ? params.properties : {}, required: Array.isArray(params.required) ? params.required.map(String) : undefined, }, }; } function list(sessionId) { const m = bySession.get(sessionId); return m ? Array.from(m.values()) : []; } function register(sessionId, tools) { if (!sessionId) throw new Error('sessionId required'); const arr = Array.isArray(tools) ? tools : [tools]; let map = bySession.get(sessionId); if (!map) { map = new Map(); bySession.set(sessionId, map); } const out = []; for (const t of arr) { const schema = normalizeSchema(t, sessionId); if (map.size >= MAX_TOOLS && !map.has(schema.name)) { throw new Error('too many custom tools (max ' + MAX_TOOLS + ')'); } map.set(schema.name, schema); if (typeof t.execute === 'function') { let h = handlers.get(sessionId); if (!h) { h = new Map(); handlers.set(sessionId, h); } h.set(schema.name, t.execute); } out.push(schema); } return list(sessionId); } function unregister(sessionId, name) { const map = bySession.get(sessionId); if (map && name) map.delete(name); const h = handlers.get(sessionId); if (h && name) h.delete(name); return list(sessionId); } function clear(sessionId) { bySession.delete(sessionId); sessionOpts.delete(sessionId); handlers.delete(sessionId); } function has(sessionId, name) { const map = bySession.get(sessionId); return !!(map && map.has(name)); } function getHandler(sessionId, name) { const h = handlers.get(sessionId); return h && h.get(name); } function defs(sessionId) { return list(sessionId); } module.exports = { NAME_RE, MAX_TOOLS, setReserved, setSession, isReserved, normalizeSchema, register, unregister, list, clear, has, getHandler, defs, };