HANDOFF · 2026-08-15 — el sujeto humano, el plan que manda, y la cara medida
⛔ SUPERSEDED por
handoff-2026-08-15b-gobernanza-en-sql.mdEste documento se conserva como la foto de su momento. Cinco de sus afirmaciones son
hoy falsas, y se listan aquí para que nadie las herede leyendo sólo esta página:
§ lo que dice lo que se midió después 3·2 «Ninguna API de gobernanza» ⛔ Falso ya entonces: /api/management/v1(C4) existía completa —8 recursos, authz, auditoría—, sólo que inerte trasENABLE_INDEX_MANAGEMENT_API. Hoy además hay/api/governance, autenticada por el token del realm3·3 «No existe OWNERSHIP» 🏁 Existe, es exclusivo por índice, y decideGrantAuthorityresponde «quién puede conceder»3·1 «conceder hoy escalaría privilegios» 🏁 Resuelto por el catalog_role dedicado, con gate de control negativo 3·2 «0 grants de objeto» Sigue siendo cierto en número, pero ya hay camino: API, SQL y UI 1 «F·1/F·2 · federación 🏁» ⚠️ La fontanería lo estaba; el dato no: ninguna persona llevaba clerk_user_id, así que el puente estaba montado y vacíoY un número suyo salió de un instrumento roto: los «109 del owner + 18 de servicios» de
datasets.created_byvenían de una sonda que clasificaba por forma UUID, cuando los
sujetos de Clerk sonuser_…(texto). El reparto real está eng5-0-fuente-del-dueno.ts.Lo que de este documento SIGUE VALIENDO: las trampas del §5, el orden acordado del §4
(que se ha completado entero) y las decisiones tomadas, que no han cambiado.
Para qué es esto. La foto autosuficiente para retomar. La sesión cerró el frente del
sujeto (Keycloak), asentó el paradigma nuevo del parser, y midió por primera vez la
superficie. Lo que queda tiene orden acordado y está al final.⚠️ SUPERSEDE a
handoff-2026-08-14-plan-gobernado.md.
Aquél sigue valiendo para el detalle de S·0-S·5; para el estado, manda éste.Regla de la casa: cada hecho lleva el comando que lo demuestra.
0 · Lo que hay que saber en diez líneas
- 🏁 Existe el SUJETO HUMANO. Hasta hoy el catálogo sólo hablaba con servicios: el realm tenía 16 clients, un usuario que nunca pidió token y cero grupos.
- 🏁 S·4 quedó CERRADO EN VIVO — la deuda que decía «no se puede afirmar que la puerta esté cerrada contra un tercero».
- 🏁 Keycloak federado con Clerk, y el gobierno le aplica a una persona (F·2 verde).
- 🏁 El plan dejó de ser suplente: el destino de escritura lo da el plan y sólo el plan.
- 🏁 La superficie está medida: 42 pantallas · 27 vivas · 6 maquetas · 0 huérfanas (con trinquete).
- ⛔ Conceder hoy escalaría privilegios en silencio — el hallazgo más peligroso, §3·1.
- ⛔ No hay API de gobernanza, ni roles de negocio (0), ni grants de objeto (0).
- ⚠️ Producción sirve
17178a5; hay 3 commits posteriores sin desplegar (docs y superficie). Ninguno cambia comportamiento de datos. - ⚠️ El cluster y Spark siguen encendidos desde el 08-14, y eso cuesta.
- ⚠️ El usuario de QA
carbon-qa-humanoquedó en el realm con su contraseña en el historial de la sesión. Rotarlo o borrarlo.
1 · Lo cerrado, con su gate
| gate | ||
|---|---|---|
| 🏁 P | Particionar por la columna de política — el techo de S·3 es un requisito comprobable | p0·p0b·p1·p2-censo-servibilidad.ts |
| 🏁 S·5·①② | El reintento OCC era heredado; las 7 propiedades de robustez, declaradas | s5-gate-reintento-occ.ts · s5d (104 tablas) · s5e |
| 🏁 K·1 | La cara deja de suponer que todo token es un servicio | 16 tests · k2-gate-sujeto-humano.ts |
| 🏁 K·3 | S·4 contra un tercero, en vivo | k3-gate-s4-en-vivo.ts |
| 🏁 F·1/F·2 | Federación Keycloak ← Clerk, y el gobierno aplica a una persona | f2-gate-federacion.ts |
| 🏁 P·4 | El plan deja de ser suplente en el destino de escritura | verificado en vivo con ALTER TABLE … ADD COLUMN |
| 🏁 SUP·0/1 | La superficie medida, y las huérfanas cerradas | npm run check:superficie |
364 tests en lib/governance · tsc limpio salvo lo de access-control-policies/, que
es trabajo en curso del owner.
2 · ⭐ Los hallazgos que ordenan lo que queda
- ⭐⭐⭐ Podar no es lo que protege. Sin partición se poda igual y queda residuo;
buckettambién. Sóloidentityresuelve el filtro. Y denegar el residual está alineado con el estándar: el issue de Iceberg #10909 —que el catálogo mande expresiones para que el motor las aplique— se cerró como not planned. El catálogo devuelve lo autorizado; no delega la aplicación. - ⭐⭐⭐ El reintento OCC ya existía, heredado del cliente Iceberg. Se estuvo a punto de construir un bucle al lado de uno que funciona.
- ⭐⭐ Un paradigma que sólo actúa cuando el otro falla no está asentado: está de guardia. El destino salía siempre de la regex y el plan sólo entraba si ésta fallaba.
- ⭐⭐ Nuestro RBAC es Polaris literal (privilegio → catalog_role → principal_role → principal, con muchos-a-muchos en medio), y la política en el plan es lo que Databricks tiene en beta. En sustrato estamos en el estándar.
- ⭐⭐ Lo que falta no es arquitectura: es superficie.
3 · ⛔ Lo que está ABIERTO, por orden de daño
3·1 · ⛔⛔ Conceder hoy escalaría privilegios, en silencio
Medido (g3-acoplamiento-de-roles.ts): sólo 2 de 7 catalog_roles son exclusivos.
ws-editor → arrastra a query-engines, sql-editor, ws-viewer
ws-viewer → arrastra a query-engines, sql-editor, ws-editor
Un GRANT MODIFY TO ws-editor escrito en el catalog_role compartido se lo daría también
a ws-viewer. ⚠️ No es un defecto del modelo de Polaris —la relación es muchos-a-muchos
por diseño—: es que hay que crear catalog_roles con propósito, no reutilizar los
compartidos. ⇒ El aplicador necesita rol dedicado antes de la primera concesión real.
3·2 · ⛔ La superficie de gobernanza no tiene con quién hablar
- Ninguna API de gobernanza: ni grants, ni privilegios, ni políticas, ni tags. Lo único
es
/api/organization/roles, que es Clerk. - Tres vocabularios de «rol» que no se hablan: los de Clerk ·
principal_rolesdel catálogo (ws-admin,sql-editor…) · los inventados de la UI (data_steward,viewer). - 0 roles de negocio y 0 grants de objeto (224 workspace + 24 schema).
3·3 · ⛔ No existe OWNERSHIP — y es la misma pieza que «quién puede conceder»
datasets.created_by significa «quién lo tecleó» (109 del owner + 18 de servicios), no
dueño. Sin dueño no hay respuesta a «¿quién tiene derecho a hacer este GRANT?», y esa
pregunta bloquea la API que la UI ya pide.
3·4 · Los demás, ya conocidos
| ⛔ | La máscara de columna no tiene camino: en el plan no se puede (medido) ⇒ esa tabla no se sirve |
| ⛔ | La Action atómica es imposible: /transactions/commit no existe en Gravitino (verificado contra el servidor) |
| ⛔ | S·5·③: que un reintento tras un 500 no duplique — sin gate |
| ⚠️ | El bridge comparte UNA sesión de Spark Connect: un fallo suyo se llevó 5 escrituras en el mismo milisegundo |
| ⚠️ | INSERT … VALUES vetado con motivo caducado; INSERT … SELECT no (su fuente no se cualifica) |
| ⚠️ | PLAN_AUDITOR sigue en shadow: le falta ③ del criterio (12 formas de usuario, hoy 0) y no se puede cerrar sin tráfico real — hacerlo con un arnés sería la trampa que el propio módulo describe |
4 · ⏭️ EL ORDEN ACORDADO — por aquí sigue la próxima sesión
① OWNERSHIP ← desbloquea «quién puede conceder»; es G·5 y SUP·3 a la vez
② Roles en KEYCLOAK, con composites ← la jerarquía de Snowflake, que Clerk no sabe modelar
③ El aplicador con catalog_role DEDICADO ← antes de la primera concesión real (§3·1)
④ La API de grants POR OBJETO ← lo que la UI de Roles & Privileges ya pide
⑤ USE SCHEMA / MANAGE GRANTS ← el cerrojo, cuando ya haya puerta
Las decisiones ya tomadas, para no repetirlas
- ⭐ A ROLES, no a personas. Un email o un
user_…en unGRANTse rechazan con un motivo que dice qué hacer. - ⭐ Keycloak = fuente de la MEMBRESÍA DE ROL; Clerk = pertenencia al workspace. No es
invertir la federación: es separar dos preguntas que hoy están fundidas en
workspace_members.role. La identidad sigue federada como la dejó F·2. - ⭐ El dueño es un ROL, no una persona (como Snowflake), y encaja con lo anterior.
- ⭐
USE SCHEMAva al final, pero se decide antes del primer grant de objeto: cambia si el modelo es puramente aditivo o tiene prerequisitos.
Lo primero de la próxima sesión
Medir qué hay para sostener OWNERSHIP antes de elegir forma: si es una columna, una
fila de privilege_grants con privilegio OWNERSHIP —como Snowflake— o las dos. La sonda
equivalente ya existe para el ámbito de objeto: g0-fuente-del-ambito-objeto.ts.
5 · 🪤 Trampas nuevas de esta sesión
| ⛔⛔ | «En local pasa y en Vercel no» = el árbol sucio. El build moría con ReferenceError: Security is not defined y npm run build local PASABA: el árbol local tenía el arreglo sin commitear y el deploy sale de git. ⇒ ante esa contradicción, git status entero, lo primero. Y filtrar su salida (grep -v) escondió justo el fichero culpable |
| ⛔⛔ | Keycloak: el PUT de usuario REEMPLAZA, no parchea. Mandar sólo {attributes} borró el email y el nombre ⇒ invalid_grant: Account is not fully set up, que no se parece a «le has borrado el perfil» |
| ⛔⛔ | Keycloak DESCARTA los atributos no declarados en el User Profile… y devuelve 204. unmanagedAttributePolicy viene DISABLED. Sin declarar clerk_user_id, la federación queda montada, el login funciona y el claim nunca llega |
| ⚠️ | El log real de un build fallido no sale de vercel --prod: sólo dice exited with 1 + «no memory problems». El bueno: npx vercel inspect <url> --logs |
| ⚠️ | Un Ready de Preview no es producción. Comprobar Environment en vercel ls |
| ⚠️ | Un gate mal diseñado FABRICA fallos que no existen: el de concurrencia acusó al sistema de perder datos y el defecto era suyo (reutilizaba cliente_id entre tandas) |
| ⚠️ | Un censo miente por la unidad que cuenta: censar page.tsx dio 14 pantallas de 42 — esta app usa un shell con pestañas. Y contar menciones en COMENTARIOS inventó 2 huérfanas de 3 |
| ⚠️ | Los scripts sin import/export son globales para TypeScript y sus constantes chocan entre ficheros — el error sale en un fichero ajeno |
6 · Comandos que se corren
# la superficie
npm run check:superficie # trinquete: 0 huérfanas
npx tsx scripts/governance/sup0-censo-de-superficie.ts
# el plano de permisos
npx dotenv -e .env.local -- npx tsx scripts/governance/g2-madurez-del-plano.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/g3-acoplamiento-de-roles.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/g0-fuente-del-ambito-objeto.ts
# el sujeto humano (KC_ADMIN_* salen de `railway variables --service keycloak --kv`)
npx tsx scripts/governance/k1-setup-sujeto-humano.ts # idempotente
npx dotenv -e .env.local -- npx tsx scripts/governance/k2-gate-sujeto-humano.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/k3-gate-s4-en-vivo.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/f2-gate-federacion.ts
# el sustrato
npx dotenv -e .env.local -- npx tsx scripts/warehouse/p1-gate-particion-gobernada.ts
S5_ESCRITORES=12 npx dotenv -e .env.local -- npx tsx scripts/warehouse/s5-gate-reintento-occ.ts
# en frío
npx vitest run lib/governance # 364
npx tsc --noEmit
# el runtime — NUNCA la consola de Vercel
curl -H "x-diag-key: $SPARK_BRIDGE_JWT_SECRET" https://app.paladio.io/api/diag/motor
7 · Estado del entorno
- Producción sirve
17178a5. Sin desplegar:e439ab3,b12cb2f,9223136(docs y superficie; ninguno cambia comportamiento de datos). - Desplegar siempre desde el worktree limpio
C:/tmp/carbon-deploy, y verificar por el contenido del runtime, nunca por elReady. SCAN_PLANNING_WORKSPACESencendido para7b500f4d…·PLAN_MANDA=on·USER_AUTHZ=enforce·PLAN_AUDITOR=shadowen prod,offen local.- Keycloak: realm
lakehouseen Railway (proyectocozy-comfort), con el IdPclerk, el clientcarbon-app, los rolesws-*y el atributoclerk_user_iddeclarado. - Tablas de prueba que quedan:
p0_part_clientes,p0_plain_clientes,p0_bucket_clientes,v_robustez_*,w8_plain_*.