Lumi/plugins/lumi_ai/backend/tool_router.js
2026-06-11 06:35:43 +02:00

44 lines
3.2 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.");
this.tools.set(def.tool_id,{required_role:"user",confirmation_required:true,risk_level:"sensitive",...def});
}
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 };