feat: rol auditor (solo lectura total) en el MCP + agente soporte

- roleCheck.js: isAuditor()/canEditContent(); canEditCode() tambien
  excluye auditor. Superficie editor/developer intacta (verificado
  contra el codigo pre-cambio en el test).
- records/tables/media/languages: tools de escritura gateadas con
  canEditContent(), orden de registro original preservado.
- agents/soporte: agente de evaluacion de incidencias en produccion,
  allowed_tools allowlist namespaceada (23 acai_code + 18 playwright,
  cero fetch), system.md con formato de informe obligatorio y trato
  del texto del cliente como dato no confiable.
- test/auditor-role.test.js: 22 tests de superficie exacta por rol.
This commit is contained in:
Jordan Diaz
2026-08-21 18:05:47 +00:00
parent 2ad2a6f87b
commit 5198afedaa
8 changed files with 487 additions and 26 deletions

View File

@@ -0,0 +1,217 @@
/**
* Smoke test del rol "auditor" (solo lectura total) y no-regresion del rol
* "editor".
*
* El rol efectivo se lee de ACAI_ROLE_OVERRIDE en CADA llamada a
* getEffectiveRole(), pero los `tools/<grupo>/index.js` lo consultan en el
* momento del REGISTRO. Por eso basta con fijar la env var antes de invocar
* las funciones de registro (no hace falta recargar modulos).
*/
import assert from "node:assert/strict";
import test from "node:test";
import { registerRecordTools } from "../tools/records/index.js";
import { registerTableTools } from "../tools/tables/index.js";
import { registerMediaTools } from "../tools/media/index.js";
import { registerLanguageTools } from "../tools/languages/index.js";
import { registerFileTools } from "../tools/files/index.js";
import { registerModuleTools } from "../tools/modules/index.js";
import { registerLayoutTools } from "../tools/layout/index.js";
import { registerHookTools } from "../tools/hooks/index.js";
import { registerLibrariesTools } from "../tools/libraries/index.js";
import { registerProjectTools } from "../tools/project/index.js";
const GROUPS = {
records: registerRecordTools,
tables: registerTableTools,
media: registerMediaTools,
languages: registerLanguageTools,
files: registerFileTools,
modules: registerModuleTools,
layout: registerLayoutTools,
hooks: registerHookTools,
libraries: registerLibrariesTools,
project: registerProjectTools,
};
/** Server MCP falso: solo acumula los nombres de tool registrados. */
function createFakeServer() {
const names = [];
return {
names,
tool(name) { names.push(name); },
};
}
function registerWithRole(role, groupName) {
const previous = process.env.ACAI_ROLE_OVERRIDE;
process.env.ACAI_ROLE_OVERRIDE = role;
try {
const server = createFakeServer();
GROUPS[groupName](server);
return server.names;
} finally {
if (previous === undefined) delete process.env.ACAI_ROLE_OVERRIDE;
else process.env.ACAI_ROLE_OVERRIDE = previous;
}
}
// ---------------------------------------------------------------------------
// Rol "auditor": SOLO lectura. Listas exactas (nombre y orden de registro).
// ---------------------------------------------------------------------------
const AUDITOR_EXPECTED = {
records: [
"list_table_records",
"get_record",
"list_page_modules",
"get_module_config_vars",
],
tables: [
"list_tables",
"get_table_schema",
],
media: [
"analyze_image",
],
languages: [
"list_web_languages",
"get_record_translations",
],
files: [
"acai-view",
"acai-glob",
"acai-grep",
],
modules: [
"check_module",
"check_module_usage",
],
layout: [
"get_layout_field",
],
hooks: [
"get_hook_middleware",
"get_hook_entryparams",
],
libraries: [
"list_global_libraries",
],
project: [
"get_web_url",
],
};
// ---------------------------------------------------------------------------
// Rol "editor": exactamente lo mismo que registraba ANTES de introducir el rol
// auditor (derivado del codigo previo al cambio). Si este bloque se rompe, la
// regresion es real.
// ---------------------------------------------------------------------------
const EDITOR_EXPECTED = {
records: [
"list_table_records",
"get_record",
"create_or_update_record",
"delete_table_records",
"add_module_to_record",
"remove_module_from_record",
"list_page_modules",
"reorder_module",
"toggle_module_visibility",
"set_module_config_vars",
"get_module_config_vars",
],
tables: [
"list_tables",
"get_table_schema",
"create_table",
"update_table_metadata",
"delete_table",
"reorder_tables",
"create_field",
"update_field",
"delete_field",
"reorder_fields",
"regenerate_enlaces",
],
media: [
"upload_record_image",
"list_record_uploads",
"replace_record_image",
"delete_record_upload",
"reorder_record_uploads",
"upload_image_to_assets",
"generate_image",
"analyze_image",
"set_upload_info",
],
languages: [
"list_web_languages",
"get_record_translations",
"set_record_translations",
],
files: [
"acai-view",
"acai-glob",
"acai-grep",
],
modules: [
"check_module",
"check_module_usage",
],
layout: [
"get_layout_field",
],
hooks: [
"get_hook_middleware",
"get_hook_entryparams",
],
libraries: [
"list_global_libraries",
],
project: [
"get_web_url",
],
};
for (const [group, expected] of Object.entries(AUDITOR_EXPECTED)) {
test(`auditor: ${group} registra solo tools de lectura`, () => {
assert.deepEqual(registerWithRole("auditor", group), expected);
});
}
for (const [group, expected] of Object.entries(EDITOR_EXPECTED)) {
test(`editor: ${group} sigue registrando lo mismo que antes`, () => {
assert.deepEqual(registerWithRole("editor", group), expected);
});
}
test("auditor: ninguna tool de escritura conocida queda expuesta", () => {
const registered = new Set(
Object.keys(GROUPS).flatMap((group) => registerWithRole("auditor", group))
);
const writeTools = [
"create_or_update_record", "delete_table_records", "add_module_to_record",
"remove_module_from_record", "reorder_module", "toggle_module_visibility",
"set_module_config_vars", "create_table", "update_table_metadata",
"delete_table", "reorder_tables", "create_field", "update_field",
"delete_field", "reorder_fields", "regenerate_enlaces",
"upload_record_image", "replace_record_image", "delete_record_upload",
"reorder_record_uploads", "upload_image_to_assets", "generate_image",
"set_upload_info", "set_record_translations", "acai-write",
"acai-line-replace", "acai-delete", "compile_module", "delete_module",
"update_module_metadata", "set_layout_field", "set_hook_middleware",
"set_hook_entryparams", "add_global_library", "remove_global_library",
"set_global_libraries", "save_project_styles",
];
for (const name of writeTools) {
assert.equal(registered.has(name), false, `tool de escritura expuesta al auditor: ${name}`);
}
});
test("developer: conserva las tools de escritura de codigo", () => {
const files = registerWithRole("developer", "files");
assert.deepEqual(files, [
"acai-view", "acai-glob", "acai-grep",
"acai-write", "acai-line-replace", "acai-delete",
]);
});