Lumi/plugins/lumi_ai/backend/tool_registry.js
2026-06-14 05:01:13 +02:00

87 lines
3.5 KiB
JavaScript

const { roleAllows } = require("./permissions");
const ROLE_RANK = Object.freeze({ user: 1, mod: 2, admin: 3 });
const FORBIDDEN_DEFINITION_KEYS = Object.freeze([
"shell",
"sql",
"filesystem",
"network",
"code_execution",
"eval"
]);
function registerManagedTool(registry, metadata, definition) {
validateManagedDefinition(metadata, definition);
const metadataRole = metadataRequiredRole(metadata.permissions);
const backendRole = normalizeRole(definition.required_role);
const requiredRole = stricterRole(metadataRole, backendRole);
const backendPermissionCheck = definition.permission_check;
const promptPermissionCheck = definition.prompt_permission_check;
const mutating = definition.mutating === true ||
["sensitive", "high", "destructive"].includes(String(definition.risk_level || metadata.risk_level || "").toLowerCase());
return registry.register({
...definition,
owning_plugin: metadata.tool_id,
required_role: requiredRole,
required_permission: String(definition.required_permission || `${metadata.tool_id}.use`),
audit_category: String(definition.audit_category || metadata.tool_type || "ai_tool"),
read_only: definition.read_only === true,
use_cases: normalizeTextArray(definition.use_cases || metadata.capabilities),
output_expectations: String(definition.output_expectations || metadata.output_expectations || ""),
confirmation_required: mutating || metadata.confirmation_required === true
? true
: definition.confirmation_required !== false,
permission_check: (input) => {
const actualRole = input.user?.isAdmin ? "admin" : input.user?.isMod ? "mod" : "user";
if (!roleAllows(actualRole, requiredRole)) return false;
return backendPermissionCheck(input) === true;
},
prompt_permission_check: typeof promptPermissionCheck === "function"
? (input) => promptPermissionCheck(input) === true
: undefined
});
}
function normalizeTextArray(value) {
return Array.isArray(value) ? value.map(String).map((entry) => entry.trim()).filter(Boolean) : [];
}
function validateManagedDefinition(metadata, definition) {
if (!definition || typeof definition !== "object") throw new Error("AI tool definition is required.");
const namespace = String(metadata.tool_namespace || metadata.tool_id);
if (!/^[a-z][a-z0-9_-]*$/i.test(namespace)) throw new Error("AI tool namespace is invalid.");
if (!String(definition.tool_id || "").startsWith(`${namespace}.`)) {
throw new Error(`Registered tool IDs must use the ${namespace}. namespace.`);
}
if (typeof definition.permission_check !== "function" || typeof definition.workflow_handler !== "function") {
throw new Error("Managed AI tools require backend permission and workflow handlers.");
}
for (const key of FORBIDDEN_DEFINITION_KEYS) {
if (definition[key] != null) throw new Error(`Managed AI tools cannot request generic ${key} access.`);
}
}
function metadataRequiredRole(permissions) {
if (typeof permissions === "string") return normalizeRole(permissions);
if (Array.isArray(permissions)) {
return permissions.map(normalizeRole).sort((a, b) => ROLE_RANK[b] - ROLE_RANK[a])[0] || "user";
}
return normalizeRole(permissions?.required_role || permissions?.role);
}
function normalizeRole(value) {
return Object.hasOwn(ROLE_RANK, value) ? value : "user";
}
function stricterRole(left, right) {
return ROLE_RANK[left] >= ROLE_RANK[right] ? left : right;
}
module.exports = {
FORBIDDEN_DEFINITION_KEYS,
registerManagedTool,
validateManagedDefinition,
metadataRequiredRole,
stricterRole
};