153 lines
4.2 KiB
JavaScript
153 lines
4.2 KiB
JavaScript
/**
|
|
* 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();
|
|
const permissions = 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);
|
|
let gates = permissions.get(sessionId);
|
|
if (!gates) permissions.set(sessionId, gates = new Map());
|
|
gates.set(schema.name, t.permission || 'write');
|
|
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);
|
|
permissions.get(sessionId)?.delete(name);
|
|
return list(sessionId);
|
|
}
|
|
|
|
function clear(sessionId) {
|
|
bySession.delete(sessionId);
|
|
sessionOpts.delete(sessionId);
|
|
handlers.delete(sessionId);
|
|
permissions.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 needsPermission(sessionId, name, mode) {
|
|
return mode !== 'always-approve' && has(sessionId, name) && permissions.get(sessionId)?.get(name) !== 'read';
|
|
}
|
|
|
|
function defs(sessionId) {
|
|
return list(sessionId);
|
|
}
|
|
|
|
module.exports = {
|
|
needsPermission,
|
|
NAME_RE,
|
|
MAX_TOOLS,
|
|
setReserved,
|
|
setSession,
|
|
isReserved,
|
|
normalizeSchema,
|
|
register,
|
|
unregister,
|
|
list,
|
|
clear,
|
|
has,
|
|
getHandler,
|
|
defs,
|
|
};
|