--- type: ARQ asset_id: ARQ-EL-IntelliBanks-MultiEmpresa-ZeroTrust-v01 version: v02 tipo: ARQ — Arquitectura de sistema status: 🟡 Draft v02 · identidad reconciliada con genniux_board (7-jul-2026) · pendiente confirmación de supuestos restantes · pendiente wargame adversarial (Opus) · pendiente ratificación Victor (L3) owner: Alex sherpa: SherpaX Alex (Fable 5) ratificador: Victor Heredia (L3) intellibank: IB-XX-Maestro fecha_creacion: 2026-07-07 proposito: > Arquitectura unificada Multi-Empresa + Zero-Trust de IntelliBanks: multi-tenancy lógica por company_id y blindaje de gobernanza en 6 capas, diseñados como una sola disciplina. Cubre modelo de identidad/tenant, contrato de la Capa 0, enforcement server-side, conmutador de empresa, migración sin pérdida y criterios de aceptación a escala. insumos: - DC-EL-IntelliBanks-DescripcionProducto-v01 (estado real del sistema) - DC-EL-IntelliBanks-AnalisisMultiEmpresa-v01 (base multi-tenant · se construye encima) - RFI-XX-IntelliBanks-BlindajeGobernanzaSync-v01 (Capas 0–5 · decisiones tomadas) - RFI-XX-IntelliBanks-NavInternaDashboards-v01 (serving por ID opaco · sub-caso) - SP-EL-IntelliBanks-PlanAccionFable5-20260707-v01 (brief + contexto verificado en código) - PROPUESTA-Integracion-Auth.md (auth genniux_board · origen=tenant · confirmado 7-jul) gate_g0: PASS · vault consultado; construye sobre análisis y RFIs existentes, no los re-deriva tags: [ARQ, intellibanks, multiempresa, multi-tenant, zero-trust, gobernanza, company-id, enforcement] --- # ARQ · IntelliBanks Multi-Empresa + Zero-Trust ## Una sola disciplina: el tenant es la primera regla de gobernanza > **Tesis de diseño.** Multi-empresa y Zero-Trust no se diseñan por separado porque son la misma pregunta con dos caras: *¿quién puede escribir qué, dónde?* La dimensión `company_id` es simplemente la **primera regla del ruleset** de la Capa 0, y el enforcement server-side (Capa 1) la valida junto con naming y ubicación en **un solo pipeline por escritura**. Todo lo demás se deriva de ahí. **Convención de este documento:** los supuestos hechos por falta de contexto van marcados **⚠️ SUPUESTO-n** y listados en §8 para confirmación de Alex. --- ## a. Diagrama de componentes y de datos ``` ┌─ CLIENTE (Electron/Angular · v≥1.1.0 gateada) ─────────────────────────┐ │ │ │ LoginPage ──► AuthService ──► token + company{id,slug} ──┐ │ │ ▼ │ │ ~/Documents/Intellibanks/ .user_session.json │ │ └── / ◄── proyección {usuario, token, │ │ ├── IB-*-.../ del tenant company_id, slug} │ │ └── .intellibanks_sync_map.json (por empresa · Fase 3) │ │ │ │ intellibanks-sync.js (daemon) │ │ · chokidar sobre / únicamente │ │ · cola secuencial; cada evento sella {company_id} al encolarse │ │ · CompanyGuard: evento.company ≠ sesión.company → DROP + log │ │ · carga governance.json (Capa 0) para pre-validar (cortesía, │ │ NUNCA autoridad — la autoridad es el servidor) │ └───────────────┬─────────────────────────────────────────────────────────┘ │ HTTPS · X-IB-Token · X-IB-Client-Version · X-IB-Gov-Version ▼ ┌─ BACKEND (PHP 5.6 · genniux.net/skills-api/api/) ───────────────────────┐ │ │ │ gate.php (incluido por TODO endpoint) │ │ 1 require_identity() → token → user → company_id (jamás del cliente)│ │ 2 require_version() → gating de versión mínima (Capa 3) │ │ 3 governance() → carga ruleset cacheado (Capa 0) │ │ │ │ validate_write(path, name, ext, content, ctx) ← upload/update/folders │ │ → ACEPTA · RECHAZA (4xx + regla violada) · CUARENTENA │ │ → atribución automática en skill_activity_logs (Capa 5) │ │ │ │ MySQL — IDENTIDAD en genniux_board (padrón único · YA existe): │ │ colaborador(wikiname, password, origen→pap_eje) ← signup.php YA lo lee │ │ pap_eje = CATÁLOGO DE TENANTS · company_id = origen │ │ puesto/type = rol │ │ MySQL — DOMINIO ai_skills Storage/Git (SIN CAMBIOS) │ │ user_tokens (NUEVA·skills-own) storage/skills//... │ │ skills(+company_id=origen, skill_id único global │ │ +status quarantine) ← UNIQUE(company_id, folder_id, name) │ │ skill_folders(+company_id=origen) ▲ mata duplicados POR DISEÑO │ │ governance_rulesets (archivo versionado en IB-XX-Maestro) │ └──────────────────────────────────────────────────────────────────────────┘ Flujo de datos de UNA escritura: archivo cambia → chokidar → cola (sella company) → CompanyGuard → pre-check Capa 0 local → POST con token → gate.php (identidad+versión) → validate_write (tenant → ruta → naming → extensión → vacío → dedup) → ACEPTA (upsert por (company,folder,name) + versión Git + log) | CUARENTENA (status + _Quarantine//) | RECHAZA (4xx con regla) → respuesta al daemon → actualiza sync map local ``` --- ## b. Modelo de identidad y tenant > **Reconciliado con genniux_board (v02, confirmado por Alex 7-jul):** el padrón de identidad **ya existe y ya está cableado** — `signup.php` YA autentica contra `colaborador` de `genniux_board`, y `pap_eje` YA es el catálogo de tenants. Por tanto **NO se crea tabla `companies` ni un segundo padrón de usuarios**: sería el anti-patrón de "dos padrones" que la propuesta de auth advierte. `company_id` **es** `colaborador.origen` (FK a `pap_eje`). Esto elimina el S-4 y simplifica la Fase 0. **Regla que separa qué es de quién (el criterio de la decisión skills_users):** *no dupliques credenciales; sí adueñate de lo que es propio de skills.* - **Identidad + credencial + catálogo de tenants → genniux_board (fuente única, intocada).** `colaborador` (quién eres + password `sha1(md5(salt+pass))`, salt `*_3Mp0W3rL485_*` — no re-hashear), `pap_eje` (organización = tenant), `puesto`/`type` (rol). `signup.php` (adaptador en skills-api, opción B de la propuesta) resuelve credencial y devuelve `{token, origen (=company_id), origen_slug, puesto/type, min_client_version, governance_version}`. - **Sesiones y tokens de dispositivo → skills, tablas NUEVAS y propias.** Aquí sí se forkea, porque genniux_board no tiene por qué guardar los tokens de dispositivo de IntelliBanks: `user_tokens(id, wikiname, token_hash, device_label, created_at, last_used_at, revoked)`. Es lo único nuevo de identidad. Token opaco PHP 5.6: `bin2hex(openssl_random_pseudo_bytes(32))`, se guarda `sha256(token)`. - **`skills_users` como padrón separado → NO se necesita.** La identidad vive en `colaborador`; el `usuario` que ya usa `ai_skills` (en `created_by`/`updated_by`) es el `wikiname`. Solo se crearía una tabla-proyección delgada si skills necesitara roles/estatus que `type`/`puesto` no expresen — hoy no es el caso. Se evita el segundo padrón. - **Denormalización de tenant en las filas de skills:** columna `company_id` (= `origen`) en `skills` y `skill_folders`, con índices `(company_id)`, `(company_id, folder_id)`, `(company_id, parent_id)`, backfill al `origen` del owner. Es denormalización para `WHERE` rápido e integridad — el catálogo sigue siendo `pap_eje`, no una tabla nueva. `skill_versions` hereda vía `skill_id`. Storage/Git intocado. **Endurecimiento de identidad — desde Fase 1, no "a mediano plazo".** Hoy `usuario` viaja en claro y cualquiera puede suplantar a otro (verificado en `auth.service.ts`); en multi-tenant es **fuga cross-empresa esperando fecha**. El camino corto del análisis (JOIN por `usuario`) queda **descartado como mecanismo final**; solo puente en shadow-mode (§f). El token de dispositivo es lo que el servidor cree. - El cliente persiste el token en `.user_session.json` (junto a `usuario`/`origen` legacy) y **todo request** lo manda en header `X-IB-Token` (fallback como parámetro `token` si el CDN interfiere — ⚠️ SUPUESTO-1). - `require_identity()` resuelve token→wikiname→origen **en el servidor**. `usuario` del cliente pasa a informativo; discrepancia token↔usuario gana el token. - **Regla de oro (invariable):** `company_id`/`origen` jamás se acepta del cliente. - Revocación: `revoked=1` mata el dispositivo (cubre "carpeta respaldada en máquina vieja que sigue sincronizando"). - **Membresía:** v1 = 1 `wikiname` ↔ 1 `origen` (como hoy en `colaborador`). Multi-membresía futura NO forzaría cambio de padrón: sería una tabla puente `user_origenes(wikiname, origen, role)` en el dominio skills, con el token portando el `origen` activo validado server-side. **Espacio global compartido (`IB-XX-Maestro`):** se reserva un `origen` especial "global" (id a definir en `pap_eje`, p. ej. `0` o una fila `XX-Global`). Lecturas: `WHERE company_id IN (, :cid)`. Escrituras a global: solo `wikiname` con rol global-write (Victor, Alex, Jay), enforceado en `validate_write`, espejo de la regla WORX. ⚠️ SUPUESTO-2 (decisión de negocio + fila en `pap_eje`, confirma Victor). **Compartir cross-empresa:** default **NO** (aislamiento duro). `share.php` valida que emisor y receptor compartan `company_id`; una excepción explícita futura requeriría registro dedicado con ratificación — fuera de alcance v01. --- ## c. Contrato de la Capa 0 — Gobernanza-as-Code por empresa **Una fuente, tres consumidores** (validador backend · SherpaX write-time · consulta humana), ahora con dimensión de tenant **sin fragmentarse**: un solo documento canónico con sección global + overrides por empresa. Vive versionado en `IB-XX-Maestro/CP-XX-IntelliBanks-Governance-v01.json` (naming BMF, historial Git como cualquier activo) y el backend lo sirve por `get_governance.php` (cacheado estilo `true_map.json`, TTL 4 min, con `governance_version` para invalidación). ```json { "governance_version": 1, "min_client_version": "1.1.0", "global": { "root_rule": "^IB-[A-Za-z0-9]{2,}-", "deny_path_segments": ["^\\."], "deny_names": [".DS_Store", "desktop.ini", "~$*", ".fuse_hidden*"], "deny_mirror_pattern": "\\.(obsidian|claude|git)(/|$)", "allow_extensions": [".md", ".html", ".docx", ".pdf", ".pptx", ".xlsx", ".csv", ".py"], "naming_bmf": "^[A-Z]{2,5}-[A-Z]{2,3}-[A-Za-z0-9]+(-[A-Za-z0-9]+)*-v[0-9]{2}", "naming_enforcement": "warn", "quarantine_root": "_Quarantine", "empty_content": "reject", "dedup_key": ["company_id", "folder_id", "name"] }, "tenants": { "": { "slug": "EmpowerLabs", "comment": "clave = colaborador.origen (FK a pap_eje); NO un id de tabla companies nueva", "roots_rw": ["IB-EL-EmpowerLabs", "IB-Clientes", "IB-BVH-Publications"], "roots_ro": ["IB-XX-Maestro", "BC-EL-BrainCodes"], "acl_overrides": { "IB-XX-Maestro": ["victor", "alex", "jay"] } } } } ``` Notas del contrato: - `naming_enforcement: "warn"` arranca en advertencia (log + tag), no rechazo — el naming BMF tiene excepciones vivas (adjuntos, imágenes, `*_html.md`) y rechazarlo en duro rompería el sync legítimo. La ruta y el tenant sí son `reject/quarantine` desde el día 1. Endurecer naming a `reject` es una decisión posterior con datos de la Capa 5. - `allow_extensions` implementa el Anexo A del RFI (política híbrida por tipo, incluye ya `.csv` y `.py`). Tipo fuera de política → **cuarentena**, nunca pérdida silenciosa. Los `.json` de estado (`rally_progress`, `team-settings`) quedan fuera del allowlist — exactamente los archivos que hoy generan duplicados dejan de sincronizar, coherente con su home canónico (local/marketplace). - El SherpaX consume el **mismo** JSON vía skill de gobernanza (Capa 4); el humano lo interroga; el backend lo enforcea. Cambias la regla una vez, se enforcea en tres lugares. - Los overrides por empresa solo pueden **restringir** sobre lo global, nunca ampliar (p. ej. no pueden re-permitir dotfolders). Evita que un tenant "abra" la gobernanza. --- ## d. Enforcement server-side (Capa 1 · keystone) Un solo pipeline en `gate.php` + `validate_write()`, incluido al inicio de **todo** endpoint (patrón `require_company()` del análisis, extendido): | # | Paso | Verifica | Si falla | |---|---|---|---| | 1 | `require_identity()` | token válido y no revocado → `user`, `company_id` | 401 (nunca "adivina" por `usuario`) | | 2 | `require_version()` | `X-IB-Client-Version ≥ min_client_version` | 426 Upgrade Required — **mata la causa #1 del incidente** | | 3 | Tenant | la ruta destino cae en `roots_rw` de SU empresa (o `roots_ro`→rechazo de escritura; ACL override para global) | 403 + log `CROSS-TENANT-ATTEMPT` | | 4 | Ruta | ningún segmento con `.` inicial, sin mirrors `archivo.*`, nunca raíz | **cuarentena** — mata dotfolders server-side (hoy solo el cliente filtra) | | 5 | Naming | regex BMF | `warn` (log + tag `naming_violation`) | | 6 | Extensión | allowlist Anexo A | **cuarentena** | | 7 | Contenido | no vacío (ya existe), hash ≠ versión actual | 400 / no-op | | 8 | **Dedup** | upsert por `UNIQUE(company_id, folder_id, name)` | imposible duplicar **por esquema** | | 9 | Persistir | versión Git + `skill_versions` | — | | 10 | Atribuir | `skill_activity_logs` +`company_id` +`verdict` +`rule_id` | — | **Dedup por diseño (bug raíz de `upload.php`):** `upload.php` deja de decidir "insert vs update" por lógica frágil; hace `SELECT id FROM skills WHERE company_id=:cid AND folder_id=:fid AND name=:name` → si existe, **delega a la ruta de update** (nueva versión del mismo `skill_id`); si no, inserta. El índice `UNIQUE` es la red final: una condición de carrera produce error de BD (reintento como update), nunca fila duplicada. Los 2,154 duplicados limpiados no pueden regresar. Precondición: re-correr `admin_cleanup.php` (analyze→backup→cleanup) **antes** de crear el índice. **Cuarentena:** fila con `status='quarantined'` + `quarantine_reason` + storage bajo `_Quarantine//` **dentro del espacio de su empresa** (la cuarentena también es per-tenant — la basura de A no la ve B). No aparece en `get_sync_map.php` normal; vista de admin permite revisar → liberar (re-valida) o borrar. Nada se pierde en silencio. **Lecturas espejo:** `get_sync_map.php`, `list_skills.php`, `search.php`, `get_skill_content.php`, `render/restaurar`, `admin_api.php` → todos con `WHERE company_id IN (0,:cid)` y validación de pertenencia por `skill_id` (que un ID filtrado de otra empresa devuelva 404, no contenido — el serving también es Zero-Trust). Duplicados y stats del admin pasan a ser por empresa; super-admin global con selector queda para Fase 3. --- ## e. Conmutador de empresa en el cliente **Contexto activo = (token, company_id, slug, carpeta local, mapa local).** Cambiar de empresa es cambiar el paquete completo, atómicamente. V1 tiene 1 empresa por usuario (el conmutador queda oculto); el diseño se implementa desde Fase 2 porque el arranque de la app ES un "switch" desde contexto nulo — mismo código, cero costo extra. Secuencia del switch (y del arranque): 1. **Freeze:** `syncSuspended=true`; la cola secuencial se drena (los eventos en vuelo terminan); chokidar `unwatch` de la carpeta actual. Reutiliza el mecanismo de pausa existente (`lastUploadAt`) elevado a bandera explícita. 2. **Swap de identidad:** si multi-membresía, el servidor emite token con la nueva empresa activa (revalida membresía server-side); `.user_session.json` se reescribe completo. 3. **Proyección local:** `WATCH_DIR` base sigue siendo `~/Documents/Intellibanks/`; la carpeta activa pasa a `/` (se crea si no existe). El mapa de sync vive **dentro de cada carpeta de empresa** (`/.intellibanks_sync_map.json`) desde Fase 2 — así N empresas coexisten en una máquina sin pisarse (adelanta lo que el análisis dejaba a Fase 3; el costo es una línea de ruta y evita una migración doble ⚠️ SUPUESTO-3). 4. **Re-sync scoped:** `get_sync_map.php` con el token nuevo devuelve solo la empresa activa; ciclo normal de arranque (stale-while-revalidate ya existente). 5. **Unfreeze:** chokidar sobre `/` únicamente; `syncSuspended=false`. **Guardas anti-escritura-cruzada (defensa en profundidad, tres capas):** - **Cliente:** cada evento de la cola sella `company_id` al encolarse; antes de POSTear, `CompanyGuard` compara contra la sesión activa — si difieren (evento encolado antes de un switch), se **descarta con log**, no se re-etiqueta. - **Cliente:** el previewer y la resolución de rutas (`getSkillFolderPath`, prefijo `/`) parten siempre de la carpeta de empresa activa; `cleanFileName` sigue siendo la única fuente de verdad de nombres, ahora con el slug (sanitizado con la misma función — seguro en macOS/Windows) antepuesto por el motor, no por el usuario. - **Servidor (la que manda):** aunque ambas guardas de cliente fallen, el paso 3 del pipeline rechaza la escritura porque el token no pertenece a la empresa destino. *El cliente propone, el servidor dispone.* **Deep-links y serving por ruta (cierra el Nav RFI y `intellibanks://`):** el serving actual por ID opaco (`/a/skill_`) rompe la navegación relativa entre dashboards. En el nuevo modelo la clave canónica es **(company_id, ruta-relativa-de-vault)** — la misma clave del sync map local. Se agrega el interceptor global en la app (propuesto por el Nav RFI): clic en link relativo → resolver ruta→ID contra el mapa local de la empresa activa → render inmediato (patrón "Skill virtual" ya probado en el previewer). `intellibanks://open?path=IB-.../BRD-...html` se vuelve la forma canónica de compartir (portable entre máquinas, scoped por tenant al resolverse); `?id=` sigue soportado como legacy. El servidor expone `resolve_path.php` (ruta→id, con validación de tenant) como fallback cuando el archivo aún no está en local. --- ## f. Plan de migración sin pérdida (estado actual → multi-tenant blindado) Precondición global (RFI §10): la nube del equipo se **resetea desde el vault limpio de Victor** (2,379 archivos, raíz solo `IB-*`) antes de encender enforcement — no se hereda basura al mundo nuevo. Cada fase es desplegable sola y compatible hacia atrás. | Fase | Qué entra | Riesgo de pérdida y su control | |---|---|---| | **0 · Estructura (invisible)** | `company_id` (=`origen`) nullable en `skills`/`skill_folders` + backfill al `origen` del owner vía `colaborador`; tabla nueva `user_tokens`; definir fila "global" en `pap_eje`; `governance.json` v1 escrito y ratificado (Fase 0 del RFI, la corre el equipo WORX). **Sin tabla `companies` — el catálogo es `pap_eje`.** | Cero — solo estructura. Sistema idéntico. | | **1a · Identidad + enforcement en sombra** | `signup.php` emite tokens; `gate.php` + `validate_write` desplegados en **shadow-mode**: validan, loguean veredictos, **no** rechazan (salvo vacíos, que ya se rechazan). Clientes viejos siguen funcionando con `usuario` legacy | Cero impacto de usuario. La sombra produce el dataset real de violaciones para calibrar reglas ANTES de enforcar (evita falsos positivos que cuarentenen trabajo legítimo). | | **1b · Keystone ON** | Limpieza dedup re-ejecutada (analyze→backup→cleanup) → índice `UNIQUE(company_id, folder_id, name)` → upsert en `upload.php` → enforcement pasa de sombra a real (tenant/ruta/extensión) → gating de versión ON | Backup SQL obligatorio (ya existe en `admin_cleanup.php`); cuarentena en vez de rechazo para todo lo ambiguo; rollback = apagar flag de enforcement (config, no deploy). | | **2 · Cliente 1.1.0 (carpeta de empresa)** | Login guarda token+empresa; migración local automatizada primera vez: (1) **candado de subidas generalizado ON** (el mecanismo `.intellibanks_upload_lock.json` deja de ser solo-JoseRojas y se vuelve parte del protocolo de migración), (2) mover árbol local a `/`, (3) reescribir claves del mapa con prefijo y moverlo dentro de la carpeta, (4) verificación de conteos local↔remoto (reusa el modal de diferencias del admin), (5) candado OFF | El candado garantiza que la mudanza local no dispare subidas; la verificación por ruta (herramienta ya construida) confirma paridad antes de reabrir. Cliente <1.1.0 ya no puede escribir (gating), así que no hay escrituras "planas" durante la ventana. | | **3 · Multi-empresa real** | Alta de 2º tenant (fila en `pap_eje`); `user_origenes` si hay multi-membresía; conmutador visible; super-admin con selector; revocación de tokens en UI | Los cimientos ya operan desde Fase 1-2; esta fase es alta de datos + UI, no cirugía. | **Orden con el resto del RFI:** Capa 2 (rollback de sync expuesto) y Capa 5 (tablero de salud) corren en paralelo desde 1b; Capa 4 (skill SherpaX) desde Fase 0 — consume el mismo `governance.json`. --- ## g. Criterios de aceptación (a escala, con adversarios) Simulación con **≥20 clientes concurrentes**, incluyendo **1 "malo"** (versión vieja + carpeta respaldada + SherpaX mal configurado) y **1 cruzando de empresa en caliente durante un sync**: - [ ] Cliente con versión < mínima: **0 escrituras aceptadas** (426), atribuidas en log. - [ ] Escritura fuera de `IB-*`, en dotfolder o mirror `archivo.*`: **0 llegan al canónico** — cuarentena per-tenant con usuario y regla. - [ ] Request forjado (token de A, ruta/ID de B; o `usuario` falsificado sin token): **403/404, 0 fugas cross-tenant**, ni en escritura ni en serving (`get_skill_content`, `render`, `restaurar`). - [ ] Switch de empresa durante sync en vuelo: los eventos encolados de la empresa anterior se descartan con log; **0 archivos aterrizan en la empresa equivocada** (verificable por `company_id` en activity log). - [ ] 100 updates rápidos del mismo path (el patrón que creaba duplicados): la tabla termina con **exactamente 1 fila** por (company, folder, name) — el índice lo garantiza, el test lo demuestra. - [ ] Raíz de cada empresa: solo `IB-*` tras la corrida completa. - [ ] Un sync malo completo es **reversible** (rollback Capa 2) sin pérdida — verificado restaurando el snapshot y comparando hashes. - [ ] Import de carpeta respaldada: pasa por diff validado + confirmación, **nunca** sync crudo. - [ ] `governance.json` modificado (p. ej. nueva extensión): los 3 consumidores lo reflejan sin deploy de código (TTL/version bump). - [ ] Todo lo anterior con GETs detrás del CDN: verificado con parámetro anti-caché (lección de la limpieza de julio). --- ## h. Riesgos abiertos y supuestos **Riesgos:** | Riesgo | Mitigación en el diseño | |---|---| | **PHP 5.6** limita cripto y ergonomía (sin `random_bytes`, sin `password_*` moderno garantizado) | `openssl_random_pseudo_bytes` + `hash('sha256')` bastan para tokens opacos; el diseño no requiere JWT. La migración de PHP queda como mejora independiente, NO bloqueante. | | **CDN cachea GETs** — un `get_sync_map` cacheado podría cruzar tenants si la clave de caché ignora el token | `get_sync_map.php` pasa a **POST** (o `Cache-Control: private` + tenant en la URL como parámetro explícito no-confiable solo para particionar caché — el servidor re-valida SIEMPRE contra el token). Criterio g-10 lo prueba. **Riesgo #1 a wargamear.** | | Falsos positivos de gobernanza cuarentenan trabajo legítimo | Shadow-mode 1a con dataset real antes de enforcar; naming en `warn`; cuarentena reversible, nunca borrado. | | `true_map.json` cacheado en servidor era global; con tenants puede servir el mapa de otra empresa | Caché **por empresa** (`true_map_.json`) — cambio de una línea, pero crítico. | | Bloqueo del equipo por gating de versión (nadie puede escribir hasta actualizar app) | Distribución del 1.1.0 ANTES de encender gating; ventana anunciada; el candado global de subidas ya existe como freno de emergencia. | | El daemon corre con el filesystem del usuario: nada impide a un usuario técnico editar `.user_session.json` | Irrelevante por diseño: el token es lo único que el servidor cree, y editarlo solo puede *quitarte* acceso. Es exactamente el punto del Zero-Trust. | **Supuestos a confirmar por Alex (⚠️):** 1. **SUPUESTO-1:** el CDN de genniux.net deja pasar headers custom (`X-IB-Token`) en POST sin strip. Si no, el token va como parámetro de body — funcional pero hay que cuidar logs de Apache (no loguear bodies). 2. **SUPUESTO-2:** `IB-XX-Maestro` como `company_id=0` global-lectura / escritura-ACL es la política que Victor quiere (vs. clonarlo por empresa). Decisión de negocio — el diseño soporta ambas, el default propuesto es global. 3. **SUPUESTO-3:** mover el mapa de sync dentro de la carpeta de empresa **desde Fase 2** (no Fase 3) es aceptable — cuesta lo mismo y evita migrar el mapa dos veces. 4. ~~**SUPUESTO-4**~~ **RESUELTO (Alex, 7-jul):** `signup.php` ya toca `colaborador` de `genniux_board` → el padrón único ya existe; se extiende para emitir el token. `pap_eje` confirmado como catálogo de tenants (`origen`=`company_id`). Pendiente menor: confirmar que el salt del hash Node (`utils/passwordHash.js`) coincide con el PHP `*_3Mp0W3rL485_*` si el login se mueve de codebase. 5. **SUPUESTO-5:** el volumen actual (~2,800 archivos, 6 usuarios → 100+) cabe en la instancia MySQL/Apache actual con los índices nuevos; no se dimensionó hardware. Falta el dato F del brief (usuarios/costos reales). 6. **SUPUESTO-6:** el historial Git del storage soporta el reset de nube del RFI §10 sin reescritura de historia (se agrega commit "estado canónico", no se hace force-push). --- ## NEXTs - [ ] **Alex** — confirmar SUPUESTOS restantes 1, 2, 3, 5, 6 (S-4 ya resuelto). Pendiente menor: paridad de salt Node↔PHP. - [ ] **Victor** — ratificar decisiones de negocio: fila "global" en `pap_eje` para `IB-XX-Maestro` (S-2), default no-compartir cross-empresa, naming en `warn`. - [ ] **Jay/equipo** — wargame adversarial con **Opus** sobre este ARQ (Anexo §4 del TP: cliente hostil, fuga cross-empresa, caos a escala) — con foco extra en el riesgo CDN. - [ ] **Equipo WORX** — escribir `CP-XX-IntelliBanks-Governance-v01.json` (Fase 0, contrato §c). - [ ] **Alex** — tras wargame + ratificación: ejecutar Fase 0 → 1a (sombra) → 1b (keystone). --- ## CHANGELOG | Versión | Fecha | Cambio | |---|---|---| | v01 | 2026-07-07 | Creación con Fable 5 (Frente 1 del SP-EL-IntelliBanks-PlanAccionFable5). Arquitectura unificada: tenant como primera regla de gobernanza; identidad por token desde Fase 1 (descarta el JOIN-por-usuario como mecanismo final); contrato Capa 0 con overrides por empresa que solo restringen; pipeline de enforcement de 10 pasos con dedup por UNIQUE(company,folder,name); conmutador con triple guarda; serving por (tenant, ruta) que absorbe Nav RFI e intellibanks://; migración en 5 fases con shadow-mode y candado generalizado; 10 criterios de aceptación adversariales; 6 supuestos marcados. | | v02 | 2026-07-07 | Identidad reconciliada con `genniux_board` tras confirmación de Alex: `signup.php` ya autentica contra `colaborador` y `pap_eje` ya es el catálogo de tenants → **se elimina la tabla `companies`** (catálogo = `pap_eje`) y `company_id`=`origen`. Decisión skills_users resuelta: NO segundo padrón de credenciales; skills solo posee `user_tokens` (y sesiones); `company_id` denormalizado en filas de skills. `governance.json` re-keyado por `origen`. S-4 resuelto; nuevo pendiente menor de paridad de salt. §a, §b, §c, §f actualizados. |