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

@@ -1,12 +1,19 @@
/**
* Helper central para determinar el rol efectivo del MCP y bloquear tools
* peligrosas cuando el user es "editor".
* peligrosas cuando el user es "editor" o "auditor".
*
* El rol se recibe principalmente via env var ACAI_ROLE_OVERRIDE inyectada
* por el backend Python (agentic.py y cronjobs.py). Hay autoderivacion
* defensiva en caso de que alguien lance el MCP sin el override:
* - Si ACAI_MODE(_OVERRIDE) = "production" → rol editor por defecto.
* - Si no → rol developer.
*
* Roles y permisos:
* - "developer": todo (codigo + contenido).
* - "editor": contenido si, codigo no.
* - "auditor": SOLO LECTURA TOTAL — ni codigo, ni contenido, ni estructura
* de BD, ni media. Se usa para evaluar incidencias en produccion sin
* tocar nada.
*/
export function getEffectiveRole() {
if (process.env.ACAI_ROLE_OVERRIDE) return process.env.ACAI_ROLE_OVERRIDE;
@@ -15,10 +22,26 @@ export function getEffectiveRole() {
return "developer";
}
/**
* True si el rol efectivo es "auditor" (solo lectura total).
*/
export function isAuditor() {
return getEffectiveRole() === "auditor";
}
/**
* True si el rol efectivo puede editar archivos de codigo.
* Los roles permitidos son todo lo que NO sea "editor".
* Los roles permitidos son todo lo que NO sea "editor" ni "auditor".
*/
export function canEditCode() {
return getEffectiveRole() !== "editor";
const role = getEffectiveRole();
return role !== "editor" && role !== "auditor";
}
/**
* True si el rol efectivo puede editar contenido (registros, tablas, media,
* traducciones). "editor" y "developer" pueden; "auditor" no.
*/
export function canEditContent() {
return !isAuditor();
}

View File

@@ -1,9 +1,14 @@
import { registerListWebLanguagesTool } from './listWebLanguages.js';
import { registerGetRecordTranslationsTool } from './getRecordTranslations.js';
import { registerSetRecordTranslationsTool } from './setRecordTranslations.js';
import { canEditContent } from '../helpers/roleCheck.js';
export function registerLanguageTools(server) {
// Lectura de idiomas/traducciones: siempre (incluido el rol auditor).
registerListWebLanguagesTool(server);
registerGetRecordTranslationsTool(server);
registerSetRecordTranslationsTool(server);
// Escritura de traducciones: developer y editor si, auditor no.
if (canEditContent()) {
registerSetRecordTranslationsTool(server);
}
}

View File

@@ -3,13 +3,29 @@ import { registerUploadImageToAssetsTool } from './uploadImageToAssets.js';
import { registerGenerateImageTool } from './generateImage.js';
import { registerAnalyzeImageTool } from './analyze_image.js';
import { registerSetUploadInfoTool } from './setUploadInfo.js';
import { canEditContent } from '../helpers/roleCheck.js';
/**
* Tools de media.
*
* Solo `analyze_image` es de lectura, asi que es la unica que ve el rol
* "auditor". El resto (subidas, generacion y metadatos de uploads) va tras
* canEditContent(). El orden de registro se mantiene igual que antes del gate
* para no alterar el listado de tools de los roles existentes.
*/
export function registerMediaTools(server) {
registerUploadRecordImageTool(server);
registerUploadImageToAssetsTool(server);
registerGenerateImageTool(server);
const canWriteContent = canEditContent();
if (canWriteContent) {
registerUploadRecordImageTool(server);
registerUploadImageToAssetsTool(server);
registerGenerateImageTool(server);
}
registerAnalyzeImageTool(server);
// Metadatos info1..info5 de uploads: son datos de contenido, no codigo,
// asi que va sin gate de canEditCode() como el resto de tools de media.
registerSetUploadInfoTool(server);
if (canWriteContent) {
// Metadatos info1..info5 de uploads: son datos de contenido, no codigo,
// asi que sigue sin gate de canEditCode() — pero si pasa por
// canEditContent(), porque el auditor no escribe nada.
registerSetUploadInfoTool(server);
}
}

View File

@@ -9,18 +9,35 @@ import { registerReorderModuleTool } from './reorderModule.js';
import { registerToggleModuleVisibilityTool } from './toggleModuleVisibility.js';
import { registerSetModuleConfigVarsTool } from './setModuleConfigVars.js';
import { registerGetModuleConfigVarsTool } from './getModuleConfigVars.js';
import { canEditContent } from '../helpers/roleCheck.js';
/**
* Tools de registros del CMS.
*
* Las de lectura se registran siempre (tambien para el rol "auditor", que es
* solo lectura total). Las de escritura van tras canEditContent(): developer
* y editor si, auditor no.
*
* El orden de registro es el mismo que antes del gate para no alterar el
* listado de tools que ven los roles existentes.
*/
export function registerRecordTools(server) {
const canWriteContent = canEditContent();
registerListTableRecordsTool(server);
registerGetRecordTool(server);
registerCreateOrUpdateRecordTool(server);
registerDeleteTableRecordsTool(server);
registerAddModuleToRecordTool(server);
registerRemoveModuleFromRecordTool(server);
if (canWriteContent) {
registerCreateOrUpdateRecordTool(server);
registerDeleteTableRecordsTool(server);
registerAddModuleToRecordTool(server);
registerRemoveModuleFromRecordTool(server);
}
registerListPageModulesTool(server);
registerReorderModuleTool(server);
registerToggleModuleVisibilityTool(server);
registerSetModuleConfigVarsTool(server);
if (canWriteContent) {
registerReorderModuleTool(server);
registerToggleModuleVisibilityTool(server);
registerSetModuleConfigVarsTool(server);
}
registerGetModuleConfigVarsTool(server);
}

View File

@@ -9,17 +9,22 @@ import { registerUpdateFieldTool } from './updateField.js';
import { registerDeleteFieldTool } from './deleteField.js';
import { registerReorderFieldsTool } from './reorderFields.js';
import { registerRegenerateEnlacesTool } from './regenerateEnlaces.js';
import { canEditContent } from '../helpers/roleCheck.js';
export function registerTableTools(server) {
// Lectura de estructura: siempre disponible (incluido el rol auditor).
registerListTablesTool(server);
registerGetTableSchemaTool(server);
registerCreateTableTool(server);
registerUpdateTableMetadataTool(server);
registerDeleteTableTool(server);
registerReorderTablesTool(server);
registerCreateFieldTool(server);
registerUpdateFieldTool(server);
registerDeleteFieldTool(server);
registerReorderFieldsTool(server);
registerRegenerateEnlacesTool(server);
// Escritura de estructura: developer y editor si, auditor no.
if (canEditContent()) {
registerCreateTableTool(server);
registerUpdateTableMetadataTool(server);
registerDeleteTableTool(server);
registerReorderTablesTool(server);
registerCreateFieldTool(server);
registerUpdateFieldTool(server);
registerDeleteFieldTool(server);
registerReorderFieldsTool(server);
registerRegenerateEnlacesTool(server);
}
}