57 lines
3.8 KiB
JavaScript
57 lines
3.8 KiB
JavaScript
const crypto = require("crypto");
|
|
const { roleAllows } = require("./permissions");
|
|
|
|
class ToolRegistry {
|
|
constructor(audit){ this.tools=new Map(); this.confirmations=new Map(); this.audit=audit; }
|
|
register(def){
|
|
if(!def?.tool_id || !def.display_name || !def.description || !def.owning_plugin || !def.required_permission || !def.audit_category || typeof def.workflow_handler!=="function" || typeof def.permission_check!=="function" || !def.schema) throw new Error("Invalid AI tool definition.");
|
|
if(this.tools.has(def.tool_id)) throw new Error(`AI tool ${def.tool_id} is already registered.`);
|
|
this.tools.set(def.tool_id,{required_role:"user",confirmation_required:true,risk_level:"sensitive",...def});
|
|
return () => this.unregister(def.tool_id, def.owning_plugin);
|
|
}
|
|
unregister(toolId, owner = null){
|
|
const def=this.tools.get(toolId); if(!def || (owner && def.owning_plugin!==owner)) return false;
|
|
this.tools.delete(toolId);
|
|
for(const [id,pending] of this.confirmations){if(pending.def?.tool_id===toolId)this.confirmations.delete(id);}
|
|
return true;
|
|
}
|
|
unregisterOwner(owner){
|
|
let removed=0;
|
|
for(const [id,def] of this.tools){if(def.owning_plugin===owner && this.unregister(id,owner))removed+=1;}
|
|
return removed;
|
|
}
|
|
list(role){ return [...this.tools.values()].filter(t=>roleAllows(role,t.required_role)).map(({workflow_handler,permission_check,...t})=>t); }
|
|
validate(tool,args,role){
|
|
const def=this.tools.get(tool); if(!def) throw new Error("Tool is not registered.");
|
|
if(!roleAllows(role,def.required_role)) throw new Error("Permission denied for this tool.");
|
|
const schema=def.schema||{}; const clean={};
|
|
for(const [key,type] of Object.entries(schema)){ const value=args?.[key]; if(type==="integer" && !Number.isInteger(Number(value))) throw new Error(`${key} must be an integer.`); if(type==="string" && typeof value!=="string") throw new Error(`${key} must be a string.`); clean[key]=type==="integer"?Number(value):value; }
|
|
return {def,args:clean};
|
|
}
|
|
prepare({tool,args,user,role,sessionId}){
|
|
const checked=this.validate(tool,args,role);
|
|
const allowed=checked.def.permission_check({user,arguments:checked.args,required_permission:checked.def.required_permission});
|
|
if(allowed && typeof allowed.then==="function")throw new Error("AI tool permission checks must be synchronous.");
|
|
if(!allowed)throw new Error("The requesting user does not have permission for this action.");
|
|
if(!checked.def.confirmation_required) return {execute:true,checked};
|
|
const id=crypto.randomUUID(); this.confirmations.set(id,{id,userId:user.id,sessionId,expiresAt:Date.now()+120000,...checked});
|
|
return {execute:false,confirmation:{id,display_name:checked.def.display_name,arguments:checked.args,expires_at:Date.now()+120000}};
|
|
}
|
|
async execute({checked,user,requestId}){
|
|
const result=await checked.def.workflow_handler({arguments:checked.args,user,initiated_via_ai:true,ai_request_id:requestId});
|
|
this.audit({kind:"tool",status:"success",user_id:user.id,tool_requested:checked.def.tool_id,tool_executed:true});
|
|
return result;
|
|
}
|
|
async confirm({id,user,sessionId}){
|
|
const pending=this.confirmations.get(id); this.confirmations.delete(id);
|
|
if(!pending || pending.expiresAt<Date.now() || pending.userId!==user.id || pending.sessionId!==sessionId) throw new Error("Confirmation is invalid or expired.");
|
|
return this.execute({checked:{def:pending.def,args:pending.args},user,requestId:id});
|
|
}
|
|
cancel(id,userId){ const p=this.confirmations.get(id); if(p?.userId===userId){this.confirmations.delete(id);return true;} return false; }
|
|
}
|
|
function parseToolCall(text){
|
|
const match=(text||"").match(/\{[\s\S]*"type"\s*:\s*"tool_call"[\s\S]*\}/); if(!match)return null;
|
|
try{const value=JSON.parse(match[0]);return value.type==="tool_call"?value:null;}catch{return null;}
|
|
}
|
|
module.exports = { ToolRegistry, parseToolCall };
|