87 lines
3.5 KiB
JavaScript
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
|
|
};
|